[
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yaml",
    "content": "name: Bug\ndescription: Report a bug\n# Create a report to help us improve\ntitle: \"[Bug] \"\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first. \n        The more information you share, the faster we can identify and fix the bug.\n      # Please check for duplicate issue first.\n  - type: textarea\n    id: Description\n    attributes:\n      label: What happened?\n      # Describe the bug\n    validations:\n      required: false\n  - type: textarea\n    id: Reproduce\n    attributes:\n      label: How we reproduce?\n      description: |\n        Reports cannot be reproduced will Most likely be closed.\n      # To Reproduce\n      value: |\n        1. ...\n        2. ...\n        3. ...\n  - type: textarea\n    id: Expected\n    attributes:\n      label: Expected behavior\n      # placeholder: |\n      # Descript what you expected to happen.\n      # Expected behavior. Descript what you expected to happen.\n  - type: textarea\n    id: Errorlog\n    attributes:\n      label: Error log\n      placeholder: |\n        Paste the error logs if any.\n      # Expected behavior. Descript what you expected to happen.\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Discord\n    url: https://discord.gg/wyshSVuvxC\n    about: Ask questions and discuss with other SafeLine users in real time.\n  - name: Home page 官网\n    url: https://waf.chaitin.com/\n    about: Get feature descriptions, technical documentation and more information. 获取功能描述、技术文档和更多信息。\n  - name: 绕过反馈\n    url: https://stack.chaitin.com/security-challenge/safeline/index\n    about: Waf 绕过可在 CT Stack 安全挑战赛提交细节"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yaml",
    "content": "name: Suggestion\n# Feature request\ndescription: New feature or improvements.\ntitle: \"[Suggestion] \"\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first.\n        Please rise only one suggestion in an issue.\n  - type: textarea\n    id: solution\n    attributes:\n      label: What would you like to be added or improved?\n      # Describe the solution you'd like\n      # placeholder: |\n      #\n  - type: textarea\n    id: problem\n    attributes:\n      label: Why is it needed?\n      # Background and the specific problem that frustrates you\n      placeholder: |\n        Background and the problem that frustrates you\n\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other.yaml",
    "content": "name: Other\ndescription: Other issues such as Doc\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please search the [open issues](https://github.com/chaitin/SafeLine/issues) and [discussion](https://github.com/chaitin/SafeLine/discussions) for duplicate issue first. \n        Please rise only one suggestion in an issue.\n  - type: textarea\n    id: content\n    attributes:\n      label: Content"
  },
  {
    "path": ".github/workflows/slmcp-docker.yml",
    "content": "name: MCP Docker\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - \"v*\"\n    paths:\n      - \"mcp_server/**\"\n      - \".github/workflows/slmcp-docker.yml\"\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          platforms: linux/amd64,linux/arm64\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERIO_USERNAME }}\n          password: ${{ secrets.DOCKERIO_PASSWORD }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: ./mcp_server\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: |\n            chaitin/safeline-mcp:latest\n            chaitin/safeline-mcp:${{ github.ref_name }}\n          cache-from: type=registry,ref=chaitin/safeline-mcp:buildcache\n          cache-to: type=registry,ref=chaitin/safeline-mcp:buildcache,mode=max\n"
  },
  {
    "path": ".gitignore",
    "content": "*.Zone.Identifier\n.DS_Store\n*.zip\n*.tar\n*.tar.gz\nbuild.sh\ncompose.yml\n__pycache__\n.cursor\n.vscode"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"blazehttp\"]\n\tpath = blazehttp\n\turl = https://github.com/chaitin/blazehttp\n\n\n[submodule \"sdk/traefik-safeline\"]\n\tpath = sdk/traefik-safeline\n\turl = https://github.com/chaitin/traefik-safeline\n"
  },
  {
    "path": "LICENSE.md",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"/images/banner.png\" width=\"400\" />\n</p>\n\n<h4 align=\"center\">\n  SafeLine - Make your web apps secure\n</h4>\n\n<p align=\"center\">\n  <a target=\"_blank\" href=\"https://ly.safepoint.cloud/laA8asp\">🏠 Website</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://ly.safepoint.cloud/w2AeHhb\">📖 Docs</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://ly.safepoint.cloud/hSMd4SH\">🔍 Live Demo</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://discord.gg/SVnZGzHFvn\">🙋‍♂️ Discord</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"/README_CN.md\">中文版</a>\n</p>\n\n## 👋 INTRODUCTION\n\nSafeLine is a self-hosted **`WAF(Web Application Firewall)`** to protect your web apps from attacks and exploits.\n\nA web application firewall helps protect web apps by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web apps from attacks such as `SQL injection`, `XSS`, `code injection`, `os command injection`, `CRLF injection`, `ldap injection`, `xpath injection`, `RCE`, `XXE`, `SSRF`, `path traversal`, `backdoor`, `bruteforce`, `http-flood`, `bot abused`, among others.\n\n#### 💡 How It Works\n\n<img src=\"/images/how-it-works.png\" width=\"800\" />\n\nBy deploying a WAF in front of a web application, a shield is placed between the web application and the Internet. While a proxy server protects a client machine’s identity by using an intermediary, a WAF is a type of reverse-proxy, protecting the server from exposure by having clients pass through the WAF before reaching the server.\n\nA WAF protects your web apps by filtering, monitoring, and blocking any malicious HTTP/S traffic traveling to the web application, and prevents any unauthorized data from leaving the app. It does this by adhering to a set of policies that help determine what traffic is malicious and what traffic is safe. Just as a proxy server acts as an intermediary to protect the identity of a client, a WAF operates in similar fashion but acting as a reverse proxy intermediary that protects the web app server from a potentially malicious client.\n\nits core capabilities include:\n\n- Defenses for web attacks\n- Proactive bot abused defense \n- HTML & JS code encryption\n- IP-based rate limiting\n- Web Access Control List\n\n#### ⚡️ Screenshots\n\n| <img src=\"./images/screenshot-1.png\" width=370 /> | <img src=\"./images/screenshot-2.png\" width=370 /> |\n| ------------------------------------------------- | ------------------------------------------------- | \n| <img src=\"./images/screenshot-3.png\" width=370 /> | <img src=\"./images/screenshot-4.png\" width=370 /> | \n\nGet [Live Demo](https://demo.waf.chaitin.com:9443/)\n\n## 🔥 FEATURES\n\nList of the main features as follows:\n\n- **`Block Web Attacks`**\n  - It defenses for all of web attacks, such as `SQL injection`, `XSS`, `code injection`, `os command injection`, `CRLF injection`, `XXE`, `SSRF`, `path traversal` and so on.\n- **`Rate Limiting`**\n  - Defend your web apps against `DoS attacks`, `bruteforce attempts`, `traffic surges`, and other types of abuse by throttling traffic that exceeds defined limits.\n- **`Anti-Bot Challenge`**\n  - Anti-Bot challenges to protect your website from `bot attacks`, humen users will be allowed, crawlers and bots will be blocked.\n- **`Authentication Challenge`**\n  - When authentication challenge turned on, visitors need to enter the password, otherwise they will be blocked.\n- **`Dynamic Protection`**\n  - When dynamic protection turned on, html and js codes in your web server will be dynamically encrypted by each time you visit.\n\n#### 🧩 Showcases\n\n|                               | Legitimate User                                     | Malicious User                                                   |\n| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- | \n| **`Block Web Attacks`**       | <img src=\"./images/skeleton.png\" width=270 />       | <img src=\"./images/blocked-for-attack-detected.png\" width=270 /> |\n| **`Rate Limiting`**           | <img src=\"./images/skeleton.png\" width=270 />       | <img src=\"./images/blocked-for-access-too-fast.png\" width=270 /> |\n| **`Anti-Bot Challenge`**       | <img src=\"./images/captcha-1.gif\" width=270 />      | <img src=\"./images/captcha-2.gif\" width=270 />                     |\n| **`Auth Challenge`**          | <img src=\"./images/auth-1.gif\" width=270 />         | <img src=\"./images/auth-2.gif\" width=270 />                        |\n| **`HTML Dynamic Protection`** | <img src=\"./images/dynamic-html-1.png\" width=270 /> | <img src=\"./images/dynamic-html-2.png\" width=270 />              |\n| **`JS Dynamic Protection`**   | <img src=\"./images/dynamic-js-1.png\" width=270 />   | <img src=\"./images/dynamic-js-2.png\" width=270 />                | \n\n## 🚀 Quickstart\n\n> [!WARNING]\n> 中国大陆用户安装国际版可能会导致无法连接云服务，请查看 [中文版安装文档](https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%AE%89%E8%A3%85%E9%9B%B7%E6%B1%A0)\n\n#### 📦 Installing\n\nInformation on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/GetStarted/Deploy)\n\n#### ⚙️ Protecting Web Apps\n\nto see [Configuration](https://docs.waf.chaitin.com/en/GetStarted/AddApplication)\n\n## 📋 More Informations\n\n#### Effect Evaluation\n\n| Metric            | ModSecurity, Level 1 | CloudFlare, Free     | SafeLine, Balance      | SafeLine, Strict      |\n| ----------------- | -------------------- | -------------------- | ---------------------- | --------------------- |\n| Total Samples     | 33669                | 33669                | 33669                  | 33669                 |\n| **Detection**     | 69.74%               | 10.70%               | 71.65%                 | **76.17%**            |\n| **False Positive**| 17.58%               | 0.07%                | **0.07%**              | 0.22%                 |\n| **Accuracy**      | 82.20%               | 98.40%               | **99.45%**             | 99.38%                |\n\n\n#### Is SafeLine Production-Ready?\n\nYes, SafeLine is production-ready.\n\n- Over 180,000 installations worldwide\n- Protecting over 1,000,000 Websites\n- Handling over 30,000,000,000 HTTP Requests Daily\n\n#### 🙋‍♂️ Community\n\nJoin our [Discord](https://discord.gg/SVnZGzHFvn) to get community support, the core team members are identified by the STAFF role in Discord.\n\n- channel [#feedback](https://discord.com/channels/1243085666485534830/1243120292822253598): for new features discussion.\n- channel [#FAQ](https://discord.com/channels/1243085666485534830/1263761679619981413): for FAQ.\n- channel [#general](https://discord.com/channels/1243085666485534830/1243115843919806486): for any other questions.\n\nSeveral contact options exist for our community, the primary one being Discord. These are in addition to GitHub issues for creating a new issue.\n\n<p align=\"left\">\n  <a target=\"_blank\" href=\"https://discord.gg/SVnZGzHFvn\"><img src=\"https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white\"></a> &nbsp;\n  <a target=\"_blank\" href=\"https://x.com/safeline_waf\"><img src=\"https://img.shields.io/badge/X.com-000000?style=flat&logo=x&logoColor=white\"></a> &nbsp;\n  <a target=\"_blank\" href=\"/images/wechat.png\"><img src=\"https://img.shields.io/badge/WeChat-07C160?style=flat&logo=wechat&logoColor=white\"></a>\n</p>\n\n#### 💪 PRO Edition\n\nComing soon!\n\n#### 📝 License\n\nSee [LICENSE](/LICENSE.md) for details.\n"
  },
  {
    "path": "README_CN.md",
    "content": "<p align=\"center\">\n  <img src=\"/images/banner.png\" width=\"400\" />\n</p>\n\n<h4 align=\"center\">\n  SafeLine - 雷池 - 不让黑客越过半步\n</h4>\n\n<p align=\"center\">\n  <a target=\"_blank\" href=\"https://waf-ce.chaitin.cn/\">🏠 官网</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://docs.waf-ce.chaitin.cn/\">📖 文档</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://demo.waf-ce.chaitin.cn:9443/\">🔍 演示环境</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"/images/wechat.png\">🙋‍♂️ 社区微信群</a> &nbsp; | &nbsp;\n  <a target=\"_blank\" href=\"https://github.com/chaitin/SafeLine\">国际版</a>\n</p>\n\n## 👋 项目介绍\n\nSafeLine，中文名 \"雷池\"，是一款简单好用, 效果突出的 **`Web 应用防火墙(WAF)`**，可以保护 Web 服务不受黑客攻击。\n\n雷池通过过滤和监控 Web 应用与互联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 `SQL 注入`、`XSS`、 `代码注入`、`命令注入`、`CRLF 注入`、`ldap 注入`、`xpath 注入`、`RCE`、`XXE`、`SSRF`、`路径遍历`、`后门`、`暴力破解`、`CC`、`爬虫` 等攻击。\n\n#### 💡 工作原理\n\n<img src=\"/images/how-it-works.png\" width=\"800\" />\n\n雷池通过阻断流向 Web 服务的恶意 HTTP 流量来保护 Web 服务。雷池作为反向代理接入网络，通过在 Web 服务前部署雷池，可在 Web 服务和互联网之间设置一道屏障。\n\n雷池的核心功能如下:\n\n- 防护 Web 攻击\n- 防爬虫, 防扫描\n- 前端代码动态加密\n- 基于源 IP 的访问速率限制\n- HTTP 访问控制\n\n#### ⚡️ 项目截图\n\n| <img src=\"./images/screenshot-1.png\" width=370 /> | <img src=\"./images/screenshot-2.png\" width=370 /> |\n| ------------------------------------------------- | ------------------------------------------------- | \n| <img src=\"./images/screenshot-3.png\" width=370 /> | <img src=\"./images/screenshot-4.png\" width=370 /> | \n\n查看 [演示环境](https://demo.waf-ce.chaitin.cn:9443/)\n\n## 🔥 核心能力\n\n对于你的网站而言, 雷池可以实现如下效果:\n\n- **`阻断 Web 攻击`**\n  - 可以防御所有的 Web 攻击，例如 `SQL 注入`、`XSS`、`代码注入`、`操作系统命令注入`、`CRLF 注入`、`XXE`、`SSRF`、`路径遍历` 等等。\n- **`限制访问频率`**\n  - 限制用户的访问速率，让 Web 服务免遭 `CC 攻击`、`暴力破解`、`流量激增` 和其他类型的滥用。\n- **`人机验证`**\n  - 互联网上有来自真人用户的流量，但更多的是由爬虫, 漏洞扫描器, 蠕虫病毒, 漏洞利用程序等自动化程序发起的流量，开启雷池的人机验证功能后真人用户会被放行，恶意爬虫将会被阻断。\n- **`身份认证`**\n  - 雷池的 \"身份认证\" 功能可以很好的解决 \"未授权访问\" 漏洞，当用户访问您的网站时，需要输入您配置的用户名和密码信息，不持有认证信息的用户将被拒之门外。\n- **`动态防护`**\n  - 在用户浏览到的网页内容不变的情况下，将网页赋予动态特性，对 HTML 和 JavaScript 代码进行动态加密，确保每次访问时这些代码都以随机且独特的形态呈现。\n\n#### 🧩 核心能力展示\n\n|                               | Legitimate User                                     | Malicious User                                                   |\n| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- | \n| **`阻断 Web 攻击`**            | <img src=\"./images/skeleton.png\" width=270 />       | <img src=\"./images/blocked-for-attack-detected.png\" width=270 /> |\n| **`限制访问频率`**             | <img src=\"./images/skeleton.png\" width=270 />       | <img src=\"./images/blocked-for-access-too-fast.png\" width=270 /> |\n| **`人机验证`**                 | <img src=\"./images/captcha-1.gif\" width=270 />      | <img src=\"./images/captcha-2.gif\" width=270 />                     |\n| **`身份认证`**                 | <img src=\"./images/auth-1.gif\" width=270 />         | <img src=\"./images/auth-2.gif\" width=270 />                        |\n| **`HTML 动态防护`**            | <img src=\"./images/dynamic-html-1.png\" width=270 /> | <img src=\"./images/dynamic-html-2.png\" width=270 />              |\n| **`JS 动态防护`**              | <img src=\"./images/dynamic-js-1.png\" width=270 />   | <img src=\"./images/dynamic-js-2.png\" width=270 />                | \n\n## 🚀 上手指南\n\n#### 📦 安装\n\n查看 [安装雷池](https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%AE%89%E8%A3%85%E9%9B%B7%E6%B1%A0)\n\n#### ⚙️ 配置防护站点\n\n查看 [快速配置](https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%BF%AB%E9%80%9F%E9%85%8D%E7%BD%AE)\n\n## 📋 更多信息\n\n#### 防护效果测试\n\n| Metric            | ModSecurity, Level 1 | CloudFlare           | 雷池, 平衡            | 雷池, 严格             |\n| ----------------- | -------------------- | -------------------- | ---------------------- | --------------------- |\n| 样本数量          | 33669                | 33669                | 33669                  | 33669                 |\n| **检出率**        | 69.74%               | 10.70%               | 71.65%                 | **76.17%**            |\n| **误报率**        | 17.58%               | 0.07%                | **0.07%**              | 0.22%                 |\n| **准确率**        | 82.20%               | 98.40%               | **99.45%**             | 99.38%                |\n\n\n#### 雷池可以投入生产使用吗\n\n是的，已经有不少用户将雷池投入生产使用，截至目前\n\n- 全球累计装机量已超过 18 万台\n- 防护的网站数量超过 100 万个\n- 每天清洗 HTTP 请求超过 300 亿次\n\n#### 🙋‍♂️ 用户社区\n\n欢迎加入雷池 [社区微信群](/images/wechat.png) 进行技术交流。\n\n也可以加入雷池 [Discord](https://discord.gg/SVnZGzHFvn) 来获取更多社区支持。\n\n<p align=\"left\">\n  <a target=\"_blank\" href=\"/images/wechat.png\"><img src=\"https://img.shields.io/badge/WeChat-07C160?style=flat&logo=wechat&logoColor=white\"></a>\n  <a target=\"_blank\" href=\"https://discord.gg/SVnZGzHFvn\"><img src=\"https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white\"></a> &nbsp;\n  <a target=\"_blank\" href=\"https://x.com/safeline_waf\"><img src=\"https://img.shields.io/badge/X.com-000000?style=flat&logo=x&logoColor=white\"></a> &nbsp;\n</p>\n\n#### 💪 专业版\n\n查看 [社区版 vs 专业版](https://waf-ce.chaitin.cn/version)\n"
  },
  {
    "path": "compose.yaml",
    "content": "networks:\n  safeline-ce:\n    name: safeline-ce\n    driver: bridge\n    ipam:\n      driver: default\n      config:\n        - gateway: ${SUBNET_PREFIX:?SUBNET_PREFIX required}.1\n          subnet: ${SUBNET_PREFIX}.0/24\n    driver_opts:\n      com.docker.network.bridge.name: safeline-ce\n\nservices:\n  postgres:\n    container_name: safeline-pg\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-postgres${ARCH_SUFFIX}:15.2\n    volumes:\n      - ${SAFELINE_DIR}/resources/postgres/data:/var/lib/postgresql/data\n      - /etc/localtime:/etc/localtime:ro\n    environment:\n      - POSTGRES_USER=safeline-ce\n      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?postgres password required}\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.2\n    command: [postgres, -c, max_connections=600]\n    healthcheck:\n      test: pg_isready -U safeline-ce -d safeline-ce\n  mgt:\n    container_name: safeline-mgt\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-mgt${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG:?image tag required}\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n      - ${SAFELINE_DIR}/resources/mgt:/app/data\n      - ${SAFELINE_DIR}/logs/nginx:/app/log/nginx:z\n      - ${SAFELINE_DIR}/resources/sock:/app/sock\n      - /var/run:/app/run\n    ports:\n      - ${MGT_PORT:-9443}:1443\n    healthcheck:\n      test: curl -k -f https://localhost:1443/api/open/health\n    environment:\n      - MGT_PG=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable\n    depends_on:\n      - postgres\n      - fvm\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"100m\"\n        max-file: \"5\"\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.4\n  detect:\n    container_name: safeline-detector\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-detector${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}\n    volumes:\n      - ${SAFELINE_DIR}/resources/detector:/resources/detector\n      - ${SAFELINE_DIR}/logs/detector:/logs/detector\n      - /etc/localtime:/etc/localtime:ro\n    environment:\n      - LOG_DIR=/logs/detector\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.5\n  tengine:\n    container_name: safeline-tengine\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-tengine${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n      - /etc/resolv.conf:/etc/resolv.conf:ro\n      - ${SAFELINE_DIR}/resources/nginx:/etc/nginx\n      - ${SAFELINE_DIR}/resources/detector:/resources/detector\n      - ${SAFELINE_DIR}/resources/chaos:/resources/chaos\n      - ${SAFELINE_DIR}/logs/nginx:/var/log/nginx:z\n      - ${SAFELINE_DIR}/resources/cache:/usr/local/nginx/cache\n      - ${SAFELINE_DIR}/resources/sock:/app/sock\n    environment:\n      - TCD_MGT_API=https://${SUBNET_PREFIX}.4:1443/api/open/publish/server\n      - TCD_SNSERVER=${SUBNET_PREFIX}.5:8000\n      # deprecated\n      - SNSERVER_ADDR=${SUBNET_PREFIX}.5:8000\n      - CHAOS_ADDR=${SUBNET_PREFIX}.10\n    ulimits:\n      nofile: 131072\n    network_mode: host\n  luigi:\n    container_name: safeline-luigi\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-luigi${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}\n    environment:\n      - MGT_IP=${SUBNET_PREFIX}.4\n      - LUIGI_PG=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n      - ${SAFELINE_DIR}/resources/luigi:/app/data\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"100m\"\n        max-file: \"5\"\n    depends_on:\n      - detect\n      - mgt\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.7\n  fvm:\n    container_name: safeline-fvm\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-fvm${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"100m\"\n        max-file: \"5\"\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.8\n  chaos:\n    container_name: safeline-chaos\n    restart: always\n    image: ${IMAGE_PREFIX}/safeline-chaos${REGION}${ARCH_SUFFIX}${RELEASE}:${IMAGE_TAG}\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"100m\"\n        max-file: \"10\"\n    environment:\n      - DB_ADDR=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-pg/safeline-ce?sslmode=disable\n    volumes:\n      - ${SAFELINE_DIR}/resources/sock:/app/sock\n      - ${SAFELINE_DIR}/resources/chaos:/app/chaos\n    networks:\n      safeline-ce:\n        ipv4_address: ${SUBNET_PREFIX}.10"
  },
  {
    "path": "management/.gitignore",
    "content": "# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n**/.idea\n*.iml\n\n#OSX system files.\n**/.DS_Store\n\n/build\n\n*.pb.go\n\nsubmodule/"
  },
  {
    "path": "management/.golangci.yml",
    "content": "linters:\n  disable-all: true\n  enable:\n    - deadcode\n    - errcheck\n    - gofmt\n    - goimports\n    - gosimple\n    - govet\n    - ineffassign\n    - staticcheck\n    - structcheck\n    - typecheck\n    - unused\n    - varcheck\n"
  },
  {
    "path": "management/Makefile",
    "content": "GO         = GO111MODULE=on go\n#GO         = GO111MODULE=on GOOS=linux GOARCH=amd64 go\nGOBUILD    = $(GO) build -mod=readonly\nGOTEST     = $(GO) test -v -p 1 -coverprofile=coverage-management.out\n\nSTAMP    = $(shell date +%s)\nGITHASH  = $(shell git rev-parse --short=8 HEAD)\nGITTAG   = $(shell git describe --tags --abbrev=0)\n\nBUILDFLAGS := -ldflags \"-X main.buildstamp=$(STAMP) -X main.githash=$(GITHASH) -X main.version=$(GITTAG)\"\npkgs        = ./...\n\nall: build-all\n\n.PHONY: build-all\nbuild-all: proto build-webserver build-tcd\n\n.PHONY: build-webserver\nbuild-webserver:\n\tcd webserver && $(GOBUILD) $(BUILDFLAGS) -o ../build/webserver main.go\n\n.PHONY: build-tcd\nbuild-tcd:\n\tcd tcontrollerd && CGO_ENABLED=0 $(GOBUILD) $(BUILDFLAGS) -o ../build/tcontrollerd main.go\n\n.PHONY: test\ntest:\n\t$(GOTEST) -failfast $(pkgs)\n\n.PHONY: proto\nproto:\n\t@./scripts/genproto.sh\n\n.PHONY: lint\nlint:\n# 'go list' needs to be executed before staticcheck to prepopulate the modules cache.\n# Otherwise staticcheck might fail randomly for some reason not yet explained.\n\t$(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null\n\tgoimports -local chaitin.cn -w $$(find . -type f -name '*.go' -not -path \"./vendor/*\")\n\tgolangci-lint version\n\tcd webserver && golangci-lint run -v --skip-dirs vendor --deadline 10m\n\n.PHONY: clean\nclean:\n\trm -rf build\n\n"
  },
  {
    "path": "management/README.md",
    "content": "# Management Micro Service\n\n## Requirements\n\nGo 1.18+\n"
  },
  {
    "path": "management/scripts/genproto.sh",
    "content": "#!/usr/bin/env bash\n#\n# Generate all protobuf bindings.\n# Run from repository root.\n\nset -u\n\nif ! [[ \"$0\" =~ scripts/genproto.sh ]]; then\n\techo \"must be run from repository root\"\n\texit 255\nfi\n\nDIRS=\"webserver/proto tcontrollerd/proto\"\n\necho \"generating code\"\nprotoc --version\nfor dir in ${DIRS}; do\n\tpushd \"${dir}\" || return\n\t  find . -type d -print0 | while IFS= read -r -d '' sdir ; do\n\t    pushd \"${sdir}\" || return\n\t      # shellcheck disable=SC2010\n\t      FS=$(ls | grep \"\\.proto\\$\")\n        if [ -n \"${FS}\" ] ; then\n          protoc --go_out=. --go_opt=paths=source_relative \\\n              --go-grpc_out=. --go-grpc_opt=paths=source_relative \\\n              \"${FS}\"\n\n          goimports -local chaitin.cn -w ./*.pb.go\n        fi\n\t    popd || return\n\t  done\n\tpopd || return\ndone\n"
  },
  {
    "path": "management/tcontrollerd/README.md",
    "content": "# TControllerD\n\nTengine Controller Daemon (abbr. TCD) will be running in the Tengine container, \ndesigned to be in place with minion on the host machine.\n\n## Requirements\n\nGo 1.18+\n\n## Development\n\n### init protobuf\n\n```shell\n# Refer: https://grpc.io/docs/languages/go/quickstart/\n# 1. Install protoc\n# https://grpc.io/docs/protoc-installation/\n\n# 2. Install Go plugins\ngo install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0\ngo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0\n\n# 3. Update your PATH so that the protoc compiler can find the plugins\n# export PATH=\"$PATH:$(go env GOPATH)/bin\"\n\n# 4. Generate proto go code\n# cd /path/to/management\n./scripts/genproto.sh\n```"
  },
  {
    "path": "management/tcontrollerd/config.yml",
    "content": "# develop use only. For production, refer to `package/build/tengine/tcontrollerd/config.yml`\nlog:\n  output: stdout              # \"stdout\", \"stderr\" or file path\n  level:  debug               # \"debug\", \"info\", \"warn\" or \"error\"\nmgt_addr: 169.254.0.4:9002        # gRPC addr of mgt-api webserver"
  },
  {
    "path": "management/tcontrollerd/controller/controller.go",
    "content": "package controller\n\nimport (\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log\"\n\tpb \"chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website\"\n)\n\nvar (\n\tlogger = log.GetLogger(\"controller\")\n)\n\nfunc Handle() error {\n\tlogger.Infof(\"Connect mgt-webserver at %s\", config.GlobalConfig.MgtWebserver)\n\tgRPCConn, err := grpc.Dial(config.GlobalConfig.MgtWebserver, []grpc.DialOption{\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t}...)\n\tif err != nil {\n\t\tlogger.Errorf(\"Fail to dial: %v\", err)\n\t\treturn err\n\t}\n\n\twsClient := pb.NewWebsiteClient(gRPCConn)\n\n\tdefer func(conn *grpc.ClientConn) {\n\t\terr := conn.Close()\n\t\tif err != nil {\n\t\t\tlogger.Errorf(\"Fail to close: %v\", err)\n\t\t\treturn\n\t\t}\n\t}(gRPCConn)\n\n\tif err = websiteHandler(wsClient); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/tcontrollerd/controller/template.go",
    "content": "package controller\n\nvar nginxConfigTpl = `\nupstream %s {\n    %s\n    keepalive 128;\n    keepalive_timeout 75;\n}\nserver {\n    %s\n    %s\n    %s\n    %s\n    location = /forbidden_page {\n        internal;\n        root /etc/nginx/forbidden_pages;\n        try_files /default_forbidden_page.html =403;\n    }\n    location ^~ / {\n        proxy_pass %s://%s;\n        include proxy_params;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header Accept-Encoding \"\";\n        t1k_append_header SL-CE-SUID %d;\n        t1k_body_size 1024k;\n        tx_body_size 4k;\n        t1k_error_page 403 /forbidden_page;\n        tx_error_page 403 /forbidden_page;\n    }\n}`\n\nvar upstreamAddrTpl = `server %s;`\nvar serverListenTpl = `listen 0.0.0.0:%s%s%s;`\nvar addrAnyPropertiesTpl = \" default_server backlog=65536 reuseport\"\nvar serverNameTpl = `server_name %s;`\nvar certTpl = `ssl_certificate /etc/nginx/certs/%s;`\nvar certKeyTpl = `ssl_certificate_key /etc/nginx/certs/%s;`\nvar backendTpl = `backend_%d`\n"
  },
  {
    "path": "management/tcontrollerd/controller/website.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"io/ioutil\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd\"\n\tpb \"chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils\"\n)\n\nconst (\n\tPing                   = \"ping\"\n\tPong                   = \"pong\"\n\tEventTypeWebsite       = \"website\"\n\tEventTypeDeleteWebsite = \"deleteWebsite\"\n\tEventTypeFullWebsite   = \"fullWebsite\"\n\n\tnginxConfigPath       = \"/etc/nginx/sites-enabled/\" // 修改的为 host /resources/nginx/ 中的文件\n\tnginxFilePrefix       = \"IF_backend_\"\n\tnginxBackupFilePrefix = \"BAK_IF_backend_\"\n\tnginxFileMode         = 0644\n\n\tHttpsScheme      = \"https\"\n\tDefaultHttpsPort = \"443\"\n)\n\nvar pong = pb.Response{Type: Pong, Msg: nil, Err: false}\n\nfunc generateNginxConfig(website *model.WebsiteConfig) (string, error) {\n\t// only ONE upstream supported in v1.0\n\tvar upstreamAddr string\n\tscheme := \"http\"\n\tfor _, upstream := range website.Upstreams {\n\t\turlInfo, err := url.Parse(upstream)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif urlInfo.Scheme == HttpsScheme && urlInfo.Port() == \"\" {\n\t\t\turlInfo.Host = urlInfo.Host + \":\" + DefaultHttpsPort\n\t\t}\n\t\tupstreamAddr = fmt.Sprintf(upstreamAddrTpl, urlInfo.Host)\n\t\tif len(urlInfo.Scheme) > 0 {\n\t\t\tscheme = urlInfo.Scheme\n\t\t}\n\t}\n\n\tsslFlag := \"\"\n\tsslCertFilename := \"\"\n\tsslCertKeyFilename := \"\"\n\tif website.KeyFilename != \"\" && website.CertFilename != \"\" {\n\t\tsslFlag = \" ssl\" // with a blank ahead\n\t\tsslCertFilename = fmt.Sprintf(certTpl, website.CertFilename)\n\t\tsslCertKeyFilename = fmt.Sprintf(certKeyTpl, website.KeyFilename)\n\t}\n\n\t// only ONE server name supported in v1.0\n\tvar serverName string\n\tvar addrAnyProperties string\n\tfor _, sn := range website.ServerNames {\n\t\tif sn == \"*\" || sn == \"\" {\n\t\t\tsn = \"_\"\n\t\t\taddrAnyProperties = addrAnyPropertiesTpl\n\t\t}\n\t\tserverName = fmt.Sprintf(serverNameTpl, sn)\n\t}\n\n\t// only ONE port supported in v1.0\n\tvar serverListen string\n\tfor _, port := range website.Ports {\n\t\tserverListen = fmt.Sprintf(serverListenTpl, port, sslFlag, addrAnyProperties)\n\t}\n\n\tupstreamName := fmt.Sprintf(backendTpl, website.Id)\n\tnginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl, upstreamName, upstreamAddr, serverListen, serverName, sslCertFilename, sslCertKeyFilename, scheme, upstreamName, website.Id))\n\treturn nginxConfig, nil\n}\n\nfunc nginxTestAndReload() error {\n\terr := ngcmd.NginxConfTest()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ngcmd.NginxConfReload()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc generateFullConfigAndReload(msg []byte) error {\n\tvar websites []model.WebsiteConfig\n\tif err := json.Unmarshal(msg, &websites); err != nil {\n\t\treturn err\n\t}\n\n\tconfigFilename := make(map[string]struct{})\n\tfor _, website := range websites {\n\t\tconfigFilename[fmt.Sprintf(\"%s%d\", nginxFilePrefix, website.Id)] = struct{}{}\n\t}\n\tfilepath.Walk(nginxConfigPath, func(path string, info fs.FileInfo, err error) error {\n\t\t_, ok := configFilename[info.Name()]\n\t\tif !ok && strings.HasPrefix(info.Name(), nginxFilePrefix) {\n\t\t\tif err := os.Remove(path); err != nil {\n\t\t\t\t// not return error only logged error in order to delete not exist website nginx conf\n\t\t\t\tlogger.Warn(err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tfor _, website := range websites {\n\t\tif err := generateConfigAndReload(&website); err != nil {\n\t\t\t// trigger a full site push when tcd starts, ignore some site errors, and push as much as possible\n\t\t\tlogger.Warn(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc generateConfigAndReload(website *model.WebsiteConfig) error {\n\tnginxConfig, err := generateNginxConfig(website)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfigPath := filepath.Join(nginxConfigPath, fmt.Sprintf(\"%s%d\", nginxFilePrefix, website.Id))\n\tbackupPath := filepath.Join(nginxConfigPath, fmt.Sprintf(\"%s%d\", nginxBackupFilePrefix, website.Id))\n\n\toldConfigExists, err := utils.FileExist(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif oldConfigExists {\n\t\toldConfig, err := ioutil.ReadFile(configPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif string(oldConfig) == nginxConfig {\n\t\t\tlogger.Info(\"No changes in the new website config, skip nginx -s reload\")\n\t\t\treturn nil\n\t\t}\n\n\t\t// tmp save old config to the backup path\n\t\tif err = utils.CopyFile(configPath, backupPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err = utils.EnsureWriteFile(configPath, []byte(nginxConfig), nginxFileMode); err != nil {\n\t\treturn err\n\t}\n\n\tif err = nginxTestAndReload(); err != nil {\n\t\tnginxError := err\n\t\tif err = os.Remove(configPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif oldConfigExists {\n\t\t\t// new config err, restore old config\n\t\t\tif err = utils.CopyFile(backupPath, configPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = os.Remove(backupPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nginxError\n\t}\n\n\tif oldConfigExists {\n\t\tif err = os.Remove(backupPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc deleteConfigAndReload(config []byte) error {\n\tvar websiteIds []uint\n\tif err := json.Unmarshal(config, &websiteIds); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, id := range websiteIds {\n\t\tconfigPath := filepath.Join(nginxConfigPath, fmt.Sprintf(\"%s%d\", nginxFilePrefix, id))\n\t\texists, err := utils.FileExist(configPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exists {\n\t\t\tif err := os.Remove(configPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := nginxTestAndReload(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc sendResponse(stream pb.Website_SubscribeClient, eventType string, errMsg []byte) error {\n\treturn stream.Send(&pb.Response{\n\t\tType: eventType,\n\t\tMsg:  errMsg,\n\t\tErr:  len(errMsg) != 0,\n\t})\n}\n\nfunc websiteHandler(wc pb.WebsiteClient) error {\n\tstream, err := wc.Subscribe(context.Background())\n\tif err != nil {\n\t\tlogger.Errorf(\"Subscribe failed: %v\", err)\n\t\treturn err\n\t}\n\n\tfor {\n\t\tevent, err := stream.Recv()\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\t// read done.\n\t\t\t\tlogger.Infof(\"Recv EOF from webserver\")\n\t\t\t\treturn nil\n\t\t\t} else {\n\t\t\t\tlogger.Errorf(\"Handle failed: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tlogger.Debugf(\"Got message Type %s, Msg: %s\", event.GetType(), event.GetMsg())\n\t\tif event.Type == Ping {\n\t\t\tif err = stream.Send(&pong); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if event.Type == EventTypeWebsite {\n\t\t\tlogger.Infof(\"Update website with config: %s\", event.GetMsg())\n\t\t\tvar website model.WebsiteConfig\n\t\t\tif err := json.Unmarshal(event.GetMsg(), &website); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err = generateConfigAndReload(&website); err != nil {\n\t\t\t\tlogger.Error(err)\n\t\t\t\tif err := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error())); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t} else if event.Type == EventTypeFullWebsite {\n\t\t\tif err = generateFullConfigAndReload(event.Msg); err != nil {\n\t\t\t\tlogger.Error(err)\n\t\t\t\tsendErr := sendResponse(stream, EventTypeFullWebsite, []byte(err.Error()))\n\t\t\t\tif sendErr != nil {\n\t\t\t\t\treturn sendErr\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err = sendResponse(stream, EventTypeFullWebsite, nil); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t} else if event.Type == EventTypeDeleteWebsite {\n\t\t\tif err = deleteConfigAndReload(event.Msg); err != nil {\n\t\t\t\tlogger.Error(err)\n\t\t\t\tsendErr := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error()))\n\t\t\t\tif sendErr != nil {\n\t\t\t\t\treturn sendErr\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "management/tcontrollerd/go.mod",
    "content": "module chaitin.cn/patronus/safeline-2/management/tcontrollerd\n\ngo 1.21\n\ntoolchain go1.21.3\n\nrequire (\n\tchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6\n\tchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c\n\tchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgoogle.golang.org/grpc v1.65.0\n)\n\nrequire (\n\tgolang.org/x/net v0.33.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/text v0.15.0 // indirect\n)\n"
  },
  {
    "path": "management/tcontrollerd/go.sum",
    "content": "chaitin.cn/dev/go/errors v0.0.0-20200717101723-df6132d53dc8/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=\nchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 h1:1Qa9ABk907/9ZrOLbbRcS8Fqq9VhjAF/mLjbSP1qAJY=\nchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=\nchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c h1:Xn9IYkxmnpDcEpV+7JIR5ufEIexd1dhqKwpOLG1mYOE=\nchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c/go.mod h1:xJIYwUoA2TX5mNg/RBrEPyE251BPwj+70/mM7UIhoxg=\nchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c h1:tXsraF7o9iUsQY6IwpDJusc6OFhB7iv/bBTfgR3MPUU=\nchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c/go.mod h1:fUvtmpG8Z8Zf5aciadL9a/vn5SB3knG7pdNJixDplPg=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=\ngolang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.39.0-dev h1:K4VkkiYp4LCvQiW6OiGglzm5nO4Zyryf7pHhzP15cmI=\ngoogle.golang.org/grpc v1.39.0-dev/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=\ngopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "management/tcontrollerd/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/controller\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/cron\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log\"\n)\n\nvar (\n\tlogger = log.GetLogger(\"main\")\n\n\tversion    = \"undefined\"\n\tgithash    = \"undefined\"\n\tbuildstamp = \"undefined\"\n\tgoVersion  = \"undefined\"\n)\n\nfunc init() {\n\t// do something that do not raise error\n}\n\nfunc handleLoop(ctx context.Context) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tos.Exit(0)\n\t\tdefault:\n\t\t\tif err := controller.Handle(); err != nil {\n\t\t\t\tlogger.Error(\"Error occurred when handling controller: \", err)\n\t\t\t}\n\t\t\ttime.Sleep(time.Second * 5)\n\t\t}\n\t}\n}\n\nfunc main() {\n\tlog.SetLogFormatter()\n\n\tfs := flag.NewFlagSet(\"tcontrollerd\", flag.ExitOnError)\n\tshowVersion := fs.Bool(\"v\", false, \"show version\")\n\tnginxTest := fs.Bool(\"t\", false, \"nginx -t\")\n\tnginxReload := fs.Bool(\"r\", false, \"nginx -s reload\")\n\tcfgFile := fs.String(\"c\", constants.ConfigFilePath, \"config file path\")\n\tif err := fs.Parse(os.Args[1:]); err != nil {\n\t\tlogger.Fatalln(\"Failed to parse args: \", err)\n\t}\n\n\tif *showVersion {\n\t\ti, _ := strconv.Atoi(buildstamp)\n\t\tt := time.Unix(int64(i), 0).Format(\"2006-01-02 15:04:05\")\n\t\tfmt.Println(\"Version:    \", version)\n\t\tfmt.Println(\"Githash:    \", githash)\n\t\tfmt.Println(\"Build:      \", t)\n\t\tfmt.Println(\"Go version: \", goVersion)\n\t\treturn\n\t}\n\n\t// init configs\n\tif err := config.InitConfigs(*cfgFile); err != nil {\n\t\tlogger.Fatalln(\"Failed to init configs: \", err)\n\t}\n\n\tif err := log.InitLogger(); err != nil {\n\t\tlogger.Fatalln(\"Failed to init db: \", err)\n\t}\n\n\tif *nginxTest {\n\t\tif err := ngcmd.NginxConfTest(); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to test nginx conf: \", err)\n\t\t}\n\t\treturn\n\t}\n\n\tif *nginxReload {\n\t\tif err := ngcmd.NginxConfReload(); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to reload nginx conf: \", err)\n\t\t}\n\t\treturn\n\t}\n\n\tif err := cron.StartCron(); err != nil {\n\t\tlogger.Fatalln(\"Failed to start cron: \", err)\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tsig := make(chan os.Signal, 1)\n\tsignal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\t<-sig\n\t\tcancel()\n\t}()\n\n\thandleLoop(ctx)\n}\n"
  },
  {
    "path": "management/tcontrollerd/model/website.go",
    "content": "package model\n\n// WebsiteConfig is supposed to be same with webserver/model/website.go\ntype WebsiteConfig struct {\n\tId           int      `json:\"id\"`\n\tServerNames  []string `json:\"server_names\"`\n\tPorts        []string `json:\"ports\"`\n\tUpstreams    []string `json:\"upstreams\"`\n\tCertFilename string   `json:\"cert_filename\"`\n\tKeyFilename  string   `json:\"key_filename\"`\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\n\t\"chaitin.cn/dev/go/settings\"\n)\n\nvar (\n\tGlobalConfig = DefaultGlobalConfig()\n)\n\nfunc InitConfigs(configFilePath string) error {\n\ts, err := settings.New(configFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.Log.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err := s.Unmarshal(\"mgt_addr\", &GlobalConfig.MgtWebserver); err != nil {\n\t\treturn err\n\t}\n\n\tif v, ok := os.LookupEnv(\"MGT_ADDR\"); ok {\n\t\tGlobalConfig.MgtWebserver = v\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/config/global.go",
    "content": "package config\n\ntype Config struct {\n\tLog          LogConfig\n\tMgtWebserver string\n}\n\nfunc DefaultGlobalConfig() Config {\n\treturn Config{\n\t\tLog:          DefaultLogConfig(),\n\t\tMgtWebserver: \"169.254.0.4:9002\",\n\t}\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/config/log.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype LogConfig struct {\n\tOutput string `yaml:\"output\"`\n\tLevel  string `yaml:\"level\"`\n}\n\nfunc DefaultLogConfig() LogConfig {\n\treturn LogConfig{\n\t\tOutput: \"stdout\",\n\t\tLevel:  \"info\",\n\t}\n}\n\nfunc (lc *LogConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"log\", lc); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/constants/constants.go",
    "content": "package constants\n\nconst (\n\tConfigFilePath = \"config.yml\"\n)\n"
  },
  {
    "path": "management/tcontrollerd/pkg/constants/forbidden_page.go",
    "content": "package constants\n\nconst (\n\tDefaultForbiddenPageMd5 = \"d9921f84f36a6cc92a6fc13946a18e98\"\n\tDefaultForbiddenPage    = ``\n)\n"
  },
  {
    "path": "management/tcontrollerd/pkg/cron/cron.go",
    "content": "package cron\n\nimport (\n\t\"github.com/robfig/cron/v3\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log\"\n)\n\nvar logger = log.GetLogger(\"cron\")\n\nfunc newCronWithSeconds() *cron.Cron {\n\tsecondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour |\n\t\tcron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)\n\treturn cron.New(cron.WithParser(secondParser), cron.WithChain())\n}\n\nfunc StartCron() error {\n\tcronInstance := newCronWithSeconds()\n\n\t_, err := cronInstance.AddFunc(specCheckForbiddenPage, checkAndUpdateForbiddenPage)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcronInstance.Start()\n\treturn nil\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/cron/forbidden_page.go",
    "content": "package cron\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"io/ioutil\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils\"\n)\n\nconst (\n\t// SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html\n\t// Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)\n\t// every 30 second (starting from 0s)\n\tspecCheckForbiddenPage = \"0/30 * * * * ?\"\n\n\tforbiddenPagePath = \"/etc/nginx/forbidden_pages/default_forbidden_page.html\"\n)\n\nfunc checkAndUpdateForbiddenPage() {\n\texisted, err := utils.FileExist(forbiddenPagePath)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\tif !existed {\n\t\terr = utils.EnsureWriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644)\n\t\tif err != nil {\n\t\t\tlogger.Error(err)\n\t\t}\n\t\treturn\n\t}\n\n\tcontent, err := ioutil.ReadFile(forbiddenPagePath)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\thash := md5.New()\n\thash.Write([]byte(content))\n\tforbiddenMd5 := hex.EncodeToString(hash.Sum(nil))\n\tif forbiddenMd5 == constants.DefaultForbiddenPageMd5 {\n\t\treturn\n\t}\n\n\terr = ioutil.WriteFile(forbiddenPagePath, []byte(constants.DefaultForbiddenPage), 0644)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/log/log.go",
    "content": "package log\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"chaitin.cn/dev/go/log\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils\"\n)\n\nfunc GetLogger(name string) *log.Logger {\n\treturn log.GetLogger(name)\n}\n\nfunc LoadLogLevel() {\n\tlv, _ := log.ParseLevel(config.GlobalConfig.Log.Level)\n\tlog.SetLevel(log.AllLoggers, lv)\n}\n\nfunc SetLogFormatter() {\n\t// format\n\tformatter := new(log.TextFormatter)\n\tformatter.FullTimestamp = true\n\tformatter.TimestampFormat = \"2006/01/02 15:04:05\"\n\tlog.SetFormatter(log.AllLoggers, formatter)\n}\n\nfunc InitLogger() error {\n\t// output\n\tswitch config.GlobalConfig.Log.Output {\n\tcase \"stdout\":\n\t\tlog.SetOutput(log.AllLoggers, os.Stdout)\n\tcase \"stderr\":\n\t\tlog.SetOutput(log.AllLoggers, os.Stderr)\n\tdefault:\n\t\texist, err := utils.FileExist(config.GlobalConfig.Log.Output)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC\n\t\tif !exist {\n\t\t\tif err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfileFlag = fileFlag | os.O_CREATE\n\t\t}\n\n\t\tif fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open log file: %s\", err.Error())\n\t\t} else {\n\t\t\tlog.SetOutput(log.AllLoggers, log.NewLockOutput(fp))\n\t\t}\n\t}\n\n\t// hook\n\tlog.AddHook(log.AllLoggers, NewRuntimeHook())\n\tlog.AddHook(log.AllLoggers, log.NewErrorStackHook(true))\n\t// level\n\tLoadLogLevel()\n\n\treturn nil\n}\n\ntype RuntimeHook struct{}\n\nfunc (h *RuntimeHook) Levels() []logrus.Level {\n\treturn logrus.AllLevels\n}\n\nfunc (h *RuntimeHook) Fire(entry *logrus.Entry) error {\n\tfile := \"???\"\n\tfuncName := \"???\"\n\tline := 0\n\n\tpc := make([]uintptr, 64)\n\t// Skip runtime.Callers, self, and another call from logrus\n\tn := runtime.Callers(3, pc)\n\tif n != 0 {\n\t\tpc = pc[:n] // pass only valid pcs to runtime.CallersFrames\n\t\tframes := runtime.CallersFrames(pc)\n\n\t\t// Loop to get frames.\n\t\t// A fixed number of pcs can expand to an indefinite number of Frames.\n\t\tfor {\n\t\t\tframe, more := frames.Next()\n\t\t\tif !strings.Contains(frame.File, \"github.com/sirupsen/logrus\") && !strings.Contains(frame.Function, \"chaitin.cn/dev/go\") {\n\t\t\t\tfile = frame.File\n\t\t\t\tfuncName = frame.Function\n\t\t\t\tline = frame.Line\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !more {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tslices := strings.Split(file, \"/\")\n\tfile = slices[len(slices)-1]\n\n\tfuncName = strings.ReplaceAll(funcName, \"chaitin.cn\", \"\")\n\n\tentry.Data[\"file\"] = file\n\tentry.Data[\"func\"] = funcName\n\tentry.Data[\"line\"] = line\n\treturn nil\n}\n\nfunc NewRuntimeHook() *RuntimeHook {\n\treturn &RuntimeHook{}\n}\n"
  },
  {
    "path": "management/tcontrollerd/pkg/ngcmd/ngcmd.go",
    "content": "package ngcmd\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log\"\n)\n\nvar logger = log.GetLogger(\"ngcmd\")\n\n// NginxConfTest exec nginx -t and return stderr\nfunc NginxConfTest() error {\n\tout, err := exec.Command(\"nginx\", \"-t\").CombinedOutput()\n\tlogger.Debugf(\"nginx -t output: %v\", string(out))\n\tif err != nil {\n\t\treturn errors.Wrap(err, string(out))\n\t}\n\n\t//logger.Debugf(\"nginx -t output: %v\", out)\n\tif strings.Contains(string(out), \"syntax is ok\") && strings.Contains(string(out), \"test is successful\") {\n\t\treturn nil\n\t} else {\n\t\treturn errors.New(fmt.Sprintf(\"nginx conf test error: %s\", string(out)))\n\t}\n}\n\n// NginxConfReload exec nginx -t and return stderr\nfunc NginxConfReload() error {\n\tout, err := exec.Command(\"nginx\", \"-s\", \"reload\").CombinedOutput()\n\tlogger.Debugf(\"nginx -s reload output: %v\", string(out))\n\tif err != nil {\n\t\treturn errors.Wrap(err, string(out))\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t} else {\n\t\treturn errors.New(fmt.Sprintf(\"nginx conf reload error: %s\", string(out)))\n\t}\n}\n"
  },
  {
    "path": "management/tcontrollerd/proto/website/website.proto",
    "content": "syntax = \"proto3\";\npackage website;\noption go_package = \"proto/website\";\n\nservice Website {\n  rpc Subscribe(stream Response) returns (stream Event) {}\n}\n\n// From client-side, may be \"pong\"\nmessage Response {\n  string type = 1;\n  bytes msg = 2;\n  bool err = 3;\n}\n\n// From server-side, may be \"ping\"\nmessage Event {\n  string type = 1;    // ping/website\n  bytes msg = 2;\n}\n"
  },
  {
    "path": "management/tcontrollerd/utils/file.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc EnsureDir(dir string) error {\n\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\treturn os.MkdirAll(dir, os.FileMode(0755))\n\t}\n\treturn nil\n}\n\nfunc EnsureFileDir(path string) error {\n\treturn EnsureDir(filepath.Dir(path))\n}\n\nfunc FileExist(path string) (bool, error) {\n\tstat, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t} else {\n\t\t\treturn false, err\n\t\t}\n\t} else {\n\t\tif stat.IsDir() {\n\t\t\treturn false, fmt.Errorf(\"%s is dir\", path)\n\t\t} else {\n\t\t\treturn true, nil\n\t\t}\n\t}\n}\n\nfunc FilesExist(paths ...string) (bool, error) {\n\tfor _, path := range paths {\n\t\texist, err := FileExist(path)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\nfunc RenameWriteFile(filename string, data []byte, perm os.FileMode) error {\n\trandFileName := filename + \".tmp.\" + RandStr(8)\n\tif err := ioutil.WriteFile(randFileName, data, perm); err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(randFileName, filename)\n}\n\nfunc EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error {\n\terr := EnsureFileDir(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn RenameWriteFile(path, data, mode)\n}\n\nfunc EnsureWriteFile(path string, data []byte, mode os.FileMode) error {\n\terr := EnsureFileDir(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn ioutil.WriteFile(path, data, mode)\n}\n\nfunc CopyFile(srcPath, dstPath string) error {\n\tsrcFile, err := os.Open(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(srcFile *os.File) {\n\t\terr := srcFile.Close()\n\t\tif err != nil {\n\n\t\t}\n\t}(srcFile)\n\n\tfileInfo, err := srcFile.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn CopyFileFromIO(srcFile, dstPath, fileInfo.Mode())\n}\n\nfunc CopyFileFromIO(src io.Reader, dstPath string, perm os.FileMode) error {\n\tif err := EnsureFileDir(dstPath); err != nil {\n\t\treturn err\n\t}\n\n\tdstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(dstFile *os.File) {\n\t\terr := dstFile.Close()\n\t\tif err != nil {\n\n\t\t}\n\t}(dstFile)\n\n\t_, err = io.Copy(dstFile, src)\n\treturn err\n}\n\nfunc CopyFileIfNotExist(srcPath, dstPath string) error {\n\tif exist, err := FileExist(dstPath); err != nil {\n\t\treturn err\n\t} else if !exist {\n\t\treturn CopyFile(srcPath, dstPath)\n\t} else {\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "management/tcontrollerd/utils/random.go",
    "content": "package utils\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n)\n\nvar letters = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\nfunc RandStr(n int) string {\n\trand.Seed(time.Now().UnixNano())\n\n\tb := make([]rune, n)\n\tfor i := range b {\n\t\tb[i] = letters[rand.Intn(len(letters))]\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "management/webserver/.gitignore",
    "content": "tmp\nTaskfile.yml\n.air.toml\n"
  },
  {
    "path": "management/webserver/README.md",
    "content": "# Web Server\n\nweb server for mgt-api\n\n## Requirements\n\nGo 1.18+\n\n## Development\n\n### Init protobuf\n\n```shell\n# Refer: https://grpc.io/docs/languages/go/quickstart/\n# 1. Install protoc\n# https://grpc.io/docs/protoc-installation/\n\n# 2. Install Go plugins\ngo install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0\ngo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0\n\n# 3. Update your PATH so that the protoc compiler can find the plugins\n# export PATH=\"$PATH:$(go env GOPATH)/bin\"\n\n# 4. Generate proto go code\n# cd /path/to/management\n./scripts/genproto.sh\n```\n\n### Init fvm libs\n\n```shell\n# Due to the fvm c header files\nmkdir -p management/webserver/submodule/fvm/\nmkdir -p management/webserver/submodule/libct/\n\ncd management/webserver/submodule/fvm/\n# Download https://chaitin.cn/patronus/fvm/-/tags 1.8.21 release:release, https://chaitin.cn/patronus/fvm/-/jobs/6716645\nunzip artifacts.zip\nrm artifacts.zip\n\ncd management/webserver/submodule/libct/\n# Download https://chaitin.cn/patronus/libct/-/tags 1.1.1.0 release, https://chaitin.cn/patronus/libct/-/jobs/7229201\n# rename\nrm artifacts.zip\n\ncd management/webserver/submodule/\n# Download https://chaitin.cn/patronus/fusion-2/-/tags 5.3.9-r1 build:release, https://chaitin.cn/patronus/fusion-2/-/jobs/7326007\n# rename\nunzip artifacts.zip\nmv artifacts/lib/libfusion.so libfvm.so\nrm artifacts.zip\nrm -r artifacts/\n```\n\n### Build\n\n```shell\ncd management/\ndocker run -it --rm -w=\"/mnt\" --mount type=bind,source=\"$(pwd)\",target=/mnt chaitin.cn/ci/golang:1.18 bash\ncp webserver/submodule/libfvm.so /usr/lib/\nmake build-webserver\n```"
  },
  {
    "path": "management/webserver/api/auth.go",
    "content": "package api\n\nimport (\n\t\"math\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log\"\n)\n\nvar logger = log.GetLogger(\"api\")\n\nvar OtpOpts = totp.GenerateOpts{\n\tIssuer:      constants.ProductName,\n\tAccountName: constants.SuperUser,\n\tPeriod:      30, // seconds\n\tDigits:      otp.DigitsSix,\n\tAlgorithm:   otp.AlgorithmSHA1,\n}\n\ntype PostLoginRequest struct {\n\tPasscode  string `json:\"passcode\"`\n\tTimestamp int64  `json:\"timestamp\"`\n}\n\nfunc PostLogin(c *gin.Context) {\n\tvar params PostLoginRequest\n\tif err := c.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\n\t// only SuperUser in v0.9\n\tvar user model.User\n\tdb.Where(&model.User{Username: constants.SuperUser}).First(&user)\n\n\tvalid := totp.Validate(params.Passcode, user.TFASecret)\n\tif !valid {\n\t\tmillisecondTimestamp := params.Timestamp\n\t\tlocalTimeStamp := time.Now()\n\t\tif millisecondTimestamp > 0 {\n\t\t\tlogger.Debugf(\"will valid otp frontend timestamp:%v, local timestamp:%v\", millisecondTimestamp, localTimeStamp)\n\t\t\totpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))\n\t\t\ttimeSub := localTimeStamp.Sub(otpTime)\n\t\t\tseconds := math.Abs(timeSub.Seconds())\n\t\t\tif seconds >= 60 {\n\t\t\t\tlogger.Errorf(\"otp timestamp gap is more than a minute\")\n\t\t\t\tresponse.Error(c, response.JSONBody{Err: response.ErrWrongTimeGap, Msg: \"otp timestamp gap is more than a minute\"}, http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: \"Failed to verify your passcode\"}, http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tuser.LastLoginTime = time.Now().Unix()\n\tuser.IsEnabled = true\n\tdb.Save(&user)\n\n\tsession := sessions.Default(c)\n\tsession.Options(sessions.Options{\n\t\tPath:   \"/\",\n\t\tMaxAge: 3600 * 24 * 7,\n\t\t//Domain: options.Domain,\n\t\t//HttpOnly: true,\n\t\t//SameSite: http.SameSiteLaxMode,\n\t\t//Secure:   false,\n\t})\n\tsession.Set(constants.DefaultSessionUserKey, user.ID)\n\tif err := session.Save(); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when creating sessions\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(c, nil)\n}\n\nfunc PostLogout(c *gin.Context) {\n\tsession := sessions.Default(c)\n\tsession.Clear()\n\n\tif err := session.Save(); err != nil {\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when creating sessions\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tresponse.Success(c, nil)\n}\n\nfunc GetOTPUrl(c *gin.Context) {\n\totpKey, err := totp.Generate(OtpOpts)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when generating otp qrcode\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\n\t// only SuperUser in v0.9\n\tuser := model.User{Username: constants.SuperUser}\n\tdb.First(&user)\n\tif user.LastLoginTime > 0 {\n\t\t// already bind tfa, because tfa binding is mandatory when login.\n\t\tresponse.Success(c, gin.H{\"url\": \"\"})\n\t\treturn\n\t}\n\n\tuser.TFASecret = otpKey.Secret()\n\tdb.Save(&user)\n\tresponse.Success(c, gin.H{\"url\": otpKey.URL()})\n}\n\nfunc GetUser(c *gin.Context) {\n\tdb := database.GetDB()\n\tuser := model.User{Username: constants.SuperUser}\n\tdb.First(&user)\n\tresponse.Success(c, gin.H{\"id\": user.ID, \"username\": user.Username})\n}\n"
  },
  {
    "path": "management/webserver/api/behaviour.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\ntype PostBehaviourRequest struct {\n\tmodel.Behaviour\n}\n\nfunc PostBehaviour(c *gin.Context) {\n\tvar params PostBehaviourRequest\n\tif err := c.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdb := database.GetDB()\n\tdb.Create(&model.Behaviour{SrcRouter: params.SrcRouter, DstRouter: params.DstRouter})\n\tresponse.Success(c, nil)\n}\n"
  },
  {
    "path": "management/webserver/api/cert.go",
    "content": "package api\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\ntype postSSLCertRequest struct {\n\tHostname string `json:\"hostname\"`\n}\n\n// SSLCertDir is the dir of tengine conf, not mgt-api nginx certs dir defined by constants.CertsPath\nconst (\n\tCRT = \".crt\"\n\tPEM = \".pem\"\n\tKEY = \".key\"\n\n\tSSLCertDir = \"certs\"\n)\n\nfunc PostUploadSSLCert(c *gin.Context) {\n\tfile, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tswitch filepath.Ext(file.Filename) {\n\tcase CRT, PEM, KEY:\n\t\tlogger.Debugf(\"File: %v is valid\", file.Filename)\n\tdefault:\n\t\tlogger.Errorf(\"Filename: %s, ext: %s\", file.Filename, filepath.Ext(file.Filename))\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrWrongFileType, Msg: \"Wrong file type, please upload a file of .crt or .key\"}, http.StatusUnsupportedMediaType)\n\t\treturn\n\t}\n\n\tvar dstPath string\n\tfilename := fmt.Sprintf(\"%s_%s\", utils.RandStr(16), file.Filename)\n\tif config.GlobalConfig.Server.DevMode {\n\t\tdstPath = filepath.Join(\"./nginx\", SSLCertDir, filename)\n\t} else {\n\t\tdstPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, filename)\n\t}\n\tif err = c.SaveUploadedFile(file, dstPath); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when saving file\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(c, gin.H{\"filename\": filename})\n}\n\nfunc PostSSLCert(c *gin.Context) {\n\tvar params postSSLCertRequest\n\tif err := c.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfilePrefix := utils.RandStr(16)\n\tcertFilename := fmt.Sprintf(\"%s_backend.crt\", filePrefix)\n\tkeyFilename := fmt.Sprintf(\"%s_backend.key\", filePrefix)\n\n\tvar certPath, keyPath string\n\tif config.GlobalConfig.Server.DevMode {\n\t\tcertPath = filepath.Join(\"./management\", SSLCertDir, certFilename)\n\t\tkeyPath = filepath.Join(\"./management\", SSLCertDir, keyFilename)\n\t} else {\n\t\tcertPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, certFilename)\n\t\tkeyPath = filepath.Join(config.GlobalConfig.NgxResDir, SSLCertDir, keyFilename)\n\t}\n\tif err := utils.WriteCertIfNotExist(\n\t\tcertPath,\n\t\tkeyPath,\n\t\tfunc() ([]byte, []byte, error) {\n\t\t\treturn utils.GenerateCert(\n\t\t\t\t[]string{params.Hostname},\n\t\t\t\t3650,\n\t\t\t\t4096,\n\t\t\t\t&pkix.Name{\n\t\t\t\t\tCountry:            []string{},\n\t\t\t\t\tProvince:           []string{},\n\t\t\t\t\tLocality:           []string{},\n\t\t\t\t\tOrganization:       []string{},\n\t\t\t\t\tOrganizationalUnit: []string{},\n\t\t\t\t\tCommonName:         \"\",\n\t\t\t\t},\n\t\t\t\tfalse,\n\t\t\t)\n\t\t}); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when generating certs\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(c, gin.H{\"crt\": certFilename, \"key\": keyFilename})\n}\n"
  },
  {
    "path": "management/webserver/api/common.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/rogpeppe/go-internal/semver\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\nconst VersionInfoEntrypoint = \"/release/latest/version.json\"\n\ntype idsRequest struct {\n\tIDs []uint `json:\"ids\" form:\"ids\"`\n}\n\ntype pageRequest struct {\n\tPage     int `json:\"page\"         form:\"page,default=1\"         binding:\"min=1\"`\n\tPageSize int `json:\"page_size\"    form:\"page_size,default=10\"   binding:\"min=1\"`\n}\n\ntype versionInfoResponse struct {\n\tLatestVersion string `json:\"latest_version\"`\n\tRecVersion    string `json:\"rec_version\"`\n}\n\nfunc GetVersion(c *gin.Context) {\n\tresponse.Success(c, gin.H{\"version\": strings.TrimPrefix(constants.Version, \"ce-\")})\n}\n\nfunc GetUpgradeTips(ctx *gin.Context) {\n\tclient := utils.GetHTTPClient()\n\tlogger.Debugf(\"GetUpgradeTips: %s\", config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint)\n\tversionInfoReq, err := http.NewRequest(http.MethodGet, config.GlobalConfig.PlatformAddr+VersionInfoEntrypoint, nil)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\tresponse.Success(ctx, gin.H{\"upgrade_tips\": constants.NotUpgrade})\n\t\treturn\n\t}\n\n\tversionInfoRsp, err := client.Do(versionInfoReq)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\tresponse.Success(ctx, gin.H{\"upgrade_tips\": constants.NotUpgrade})\n\t\treturn\n\t}\n\tbody, err := ioutil.ReadAll(versionInfoRsp.Body)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\tresponse.Success(ctx, gin.H{\"upgrade_tips\": constants.NotUpgrade})\n\t\treturn\n\t}\n\n\tversionInfo := &versionInfoResponse{}\n\terr = json.Unmarshal(body, versionInfo)\n\tif err != nil {\n\t\tlogger.Warnf(\"err: %v, body: %s\", err, body)\n\t\tresponse.Success(ctx, gin.H{\"upgrade_tips\": constants.NotUpgrade})\n\t\treturn\n\t}\n\n\tcurrentVersion := fmt.Sprintf(\"v%s\", constants.Version)\n\tlatestVersionCmp := semver.Compare(currentVersion, versionInfo.LatestVersion)\n\trecVersionCmp := semver.Compare(currentVersion, versionInfo.RecVersion)\n\tif semver.Compare(versionInfo.LatestVersion, versionInfo.RecVersion) == -1 || latestVersionCmp == 1 {\n\t\tlogger.Warnf(\"The version number is invalid, current version: %s, latest version: %s, rec version: %s\",\n\t\t\tcurrentVersion, versionInfo.LatestVersion, versionInfo.RecVersion)\n\t\tresponse.Success(ctx, gin.H{\"upgrade_tips\": constants.NotUpgrade})\n\t\treturn\n\t}\n\n\tvar upgradeTips int\n\tif recVersionCmp == -1 {\n\t\tupgradeTips = constants.MustUpgrade\n\t} else if recVersionCmp == 0 {\n\t\tif latestVersionCmp == 0 {\n\t\t\tupgradeTips = constants.NotUpgrade\n\t\t} else {\n\t\t\tupgradeTips = constants.RecommendedUpgrade\n\t\t}\n\t} else {\n\t\tif latestVersionCmp < 0 {\n\t\t\tupgradeTips = constants.RecommendedUpgrade\n\t\t} else {\n\t\t\tupgradeTips = constants.NotUpgrade\n\t\t}\n\t}\n\n\tresponse.Success(ctx, gin.H{\"upgrade_tips\": upgradeTips})\n}\n"
  },
  {
    "path": "management/webserver/api/dashboard.go",
    "content": "package api\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\nfunc GetDashboardCounts(c *gin.Context) {\n\tvar requested int64 = 0\n\tvar intercepted int64 = 0\n\tdb := database.GetDB()\n\n\tvar statisticTotalReq model.SystemStatistics\n\tr := db.Where(\"type = 'total-req'\").Where(\"created_at >= date_trunc('day',now())\").First(&statisticTotalReq)\n\tif r.RowsAffected > 0 {\n\t\trequested = statisticTotalReq.Value\n\t}\n\n\tvar statisticTotalDenied model.SystemStatistics\n\tr = db.Where(\"type = 'total-denied'\").Where(\"created_at >= date_trunc('day',now())\").First(&statisticTotalDenied)\n\tif r.RowsAffected > 0 {\n\t\tintercepted = statisticTotalDenied.Value\n\t}\n\tresponse.Success(c, gin.H{\"requested\": requested, \"intercepted\": intercepted})\n}\n\nfunc GetDashboardSites(c *gin.Context) {\n\tresponse.Success(c, gin.H{\"normal\": 0, \"abnormal\": 0})\n}\n\nfunc GetDashboardQps(c *gin.Context) {\n\n\tvar statistics []model.SystemStatistics\n\n\tdb := database.GetDB()\n\tdb.Where(\"type = 'req'\").Order(\"created_at desc\").Limit(75).Find(&statistics)\n\n\ttype Node struct {\n\t\tLabel string `json:\"label\"`\n\t\tValue int64  `json:\"value\"`\n\t}\n\tvar nodes = make([]Node, 0)\n\n\tfor i := len(statistics) - 1; i >= 0; i-- {\n\t\tnodes = append(nodes, Node{\n\t\t\tLabel: statistics[i].CreatedAt.Format(\"2006-01-02 15:04:05\"),\n\t\t\tValue: int64(math.Ceil(float64(statistics[i].Value) / 5)),\n\t\t})\n\t}\n\n\tresponse.Success(c, gin.H{\"nodes\": nodes, \"total\": len(nodes)})\n}\n\nfunc GetDashboardRequests(c *gin.Context) {\n\n\tvar statistics []model.SystemStatistics\n\n\tdb := database.GetDB()\n\tdb.Where(\"type = 'total-req'\").Order(\"created_at desc\").Limit(30).Find(&statistics)\n\n\ttype Node struct {\n\t\tLabel string `json:\"label\"`\n\t\tValue int64  `json:\"value\"`\n\t}\n\tvar nodes = make([]Node, 0)\n\n\tfor i := 30; i > len(statistics); i-- {\n\t\tnodes = append(nodes, Node{\n\t\t\tLabel: time.Now().Add(-time.Duration(i-1) * time.Hour * 24).Format(\"2006-01-02\"),\n\t\t\tValue: 0,\n\t\t})\n\t}\n\n\tfor i := len(statistics) - 1; i >= 0; i-- {\n\t\tnodes = append(nodes, Node{\n\t\t\tLabel: statistics[i].CreatedAt.Format(\"2006-01-02\"),\n\t\t\tValue: statistics[i].Value,\n\t\t})\n\t}\n\n\tresponse.Success(c, gin.H{\"nodes\": nodes, \"total\": len(nodes)})\n}\n\nfunc GetDashboardIntercepts(c *gin.Context) {\n\tvar statistics []model.SystemStatistics\n\n\tdb := database.GetDB()\n\tdb.Where(\"type = 'total-denied'\").Order(\"created_at desc\").Limit(30).Find(&statistics)\n\n\ttype Node struct {\n\t\tLabel string `json:\"label\"`\n\t\tValue int64  `json:\"value\"`\n\t}\n\tvar nodes = make([]Node, 0)\n\n\tfor i := 30; i > len(statistics); i-- {\n\t\tnodes = append(nodes, Node{\n\t\t\tLabel: time.Now().Add(-time.Duration(i-1) * time.Hour * 24).Format(\"2006-01-02\"),\n\t\t\tValue: 0,\n\t\t})\n\t}\n\n\tfor i := len(statistics) - 1; i >= 0; i-- {\n\t\tnodes = append(nodes, Node{\n\t\t\tLabel: statistics[i].CreatedAt.Format(\"2006-01-02\"),\n\t\t\tValue: statistics[i].Value,\n\t\t})\n\t}\n\n\tresponse.Success(c, gin.H{\"nodes\": nodes, \"total\": len(nodes)})\n}\n"
  },
  {
    "path": "management/webserver/api/detectlog.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/dev/go/log\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\ntype (\n\tGetDetectLogDetailRequest struct {\n\t\tEventId string `json:\"event_id\"   form:\"event_id\"`\n\t}\n\n\tPostFalsePositivesRequest struct {\n\t\tEventId string `json:\"event_id\"`\n\t}\n\n\ttelemetryFalsePositives struct {\n\t\tTelemetry struct {\n\t\t\tId string `json:\"id\"`\n\t\t} `json:\"telemetry\"`\n\n\t\tSafeline struct {\n\t\t\tId        string          `json:\"id\"`\n\t\t\tType      string          `json:\"type\"`\n\t\t\tDetectLog model.DetectLog `json:\"detect_log\"`\n\t\t} `json:\"safeline\"`\n\t}\n)\n\nfunc getDetectLog(eventId string) (*model.DetectLog, error) {\n\tdb := database.GetDB()\n\tvar detectLogBasic model.DetectLogBasic\n\tres := db.Where(&model.DetectLogBasic{EventId: eventId}).First(&detectLogBasic)\n\tif res.RowsAffected == 0 {\n\t\treturn nil, errors.New(\"Data queried does not exist\")\n\t}\n\n\tvar detectLogDetail model.DetectLogDetail\n\tdb.Where(&model.DetectLogDetail{EventId: eventId}).First(&detectLogDetail)\n\n\tdetectLog, err := model.TransformDetectLog(&detectLogBasic, &detectLogDetail)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn detectLog, nil\n}\n\nfunc GetDetectLogList(c *gin.Context) {\n\tvar params pageRequest\n\tif err := c.BindQuery(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdb := database.GetDB()\n\n\ttx := db.Where(\"\")\n\t// 按 ip 搜索条件\n\tif ip := c.Query(\"ip\"); ip != \"\" {\n\t\ttx = tx.Where(\"src_ip = ?\", ip)\n\t}\n\t// 按 url 搜索条件\n\tif url := c.Query(\"url\"); url != \"\" {\n\t\ttx = tx.Where(\"url_path like ?\", \"%\"+url+\"%\")\n\t}\n\t// 按 type 搜索条件\n\tif at := c.Query(\"attack_type\"); at != \"\" {\n\t\tns := make([]int, 0)\n\t\tfor _, s := range strings.Split(at, \",\") {\n\t\t\tn, err := strconv.Atoi(s)\n\t\t\tif err == nil {\n\t\t\t\tns = append(ns, n)\n\t\t\t}\n\t\t}\n\t\ttx = tx.Where(\"attack_type in (?)\", ns)\n\t}\n\n\tvar total int64\n\ttx.Model(&model.DetectLogBasic{}).Count(&total)\n\n\tvar basicList []model.DetectLogBasic\n\ttx.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order(\"id desc\").Find(&basicList)\n\tvar dLogList []*model.DetectLog\n\tfor _, basic := range basicList {\n\t\tdLog, err := model.TransformDetectLog(&basic, nil)\n\t\tif err != nil {\n\t\t\tlogger.Warn(err)\n\t\t\tcontinue\n\t\t}\n\t\tdLogList = append(dLogList, dLog)\n\t}\n\tresponse.Success(c, gin.H{\"data\": dLogList, \"total\": total})\n}\n\nfunc GetDetectLogDetail(c *gin.Context) {\n\tvar params GetDetectLogDetailRequest\n\tif err := c.BindQuery(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdetectLog, err := getDetectLog(params.EventId)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorDataNotExist, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tresponse.Success(c, detectLog)\n}\n\nfunc PostFalsePositives(c *gin.Context) {\n\tvar params PostFalsePositivesRequest\n\tif err := c.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdetectLog, err := getDetectLog(params.EventId)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(c, response.ErrorDataNotExist, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\tvar option model.Options\n\tdb.Where(&model.Options{Key: constants.MachineID}).First(&option)\n\n\tvar jsonData telemetryFalsePositives\n\tjsonData.Telemetry.Id = constants.TelemetryId\n\tjsonData.Safeline.Id = option.Value\n\tjsonData.Safeline.Type = constants.FalsePositives\n\tjsonData.Safeline.DetectLog = *detectLog\n\n\tdata, err := json.Marshal(jsonData)\n\tif err != nil {\n\t\tlog.Warn(err)\n\t\tresponse.Success(c, nil)\n\t\treturn\n\t}\n\treader := bytes.NewReader(data)\n\n\tclient := utils.GetHTTPClient()\n\taddr := config.GlobalConfig.Telemetry.Addr\n\n\trsp, err := pkg.DoPostTelemetry(client, addr, reader)\n\tif err != nil {\n\t\tlog.Warn(err)\n\t\tresponse.Success(c, nil)\n\t\treturn\n\t}\n\n\tif rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusCreated {\n\t\tlog.Warn(fmt.Sprintf(\"Transfer telemetry failed, status code = %d\", rsp.StatusCode), err)\n\t\tresponse.Success(c, nil)\n\t\treturn\n\t}\n\n\tresponse.Success(c, nil)\n}\n"
  },
  {
    "path": "management/webserver/api/endpoints.go",
    "content": "package api\n\nconst (\n\tVersion             = \"/Version\"\n\tUpgradeTips         = \"/UpgradeTips\"\n\tLogin               = \"/Login\"\n\tLogout              = \"/Logout\"\n\tOTPUrl              = \"/OTPUrl\"\n\tUser                = \"/User\"\n\tDetectLogList       = \"/DetectLogList\"\n\tDetectLogDetail     = \"/DetectLogDetail\"\n\tBehaviour           = \"/Behaviour\"\n\tFalsePositives      = \"/FalsePositives\"\n\tWebsite             = \"/Website\"\n\tUploadSSLCert       = \"/UploadSSLCert\"\n\tSSLCert             = \"/SSLCert\"\n\tPolicyRule          = \"/PolicyRule\"\n\tSwitchPolicyRule    = \"/SwitchPolicyRule\"\n\tDashboardCounts     = \"/dashboard/counts\"\n\tDashboardSites      = \"/dashboard/sites\"\n\tDashboardQps        = \"/dashboard/qps\"\n\tDashboardRequests   = \"/dashboard/requests\"\n\tDashboardIntercepts = \"/dashboard/intercepts\"\n\tPolicyGroupGlobal   = \"/PolicyGroupGlobal\"\n\tSrcIPConfig         = \"/SrcIPConfig\"\n)\n"
  },
  {
    "path": "management/webserver/api/policygroup.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"gorm.io/gorm\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm\"\n)\n\nfunc PutPolicyGroupGlobal(ctx *gin.Context) {\n\tvar params model.PolicyGroup\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tvar pggOption model.Options\n\t\tres := tx.Where(&model.Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tpggStr, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\tlogger.Error(err)\n\t\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\t\treturn err\n\t\t}\n\n\t\tpggOption.Value = string(pggStr)\n\t\ttx.Save(&pggOption)\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc GetPolicyGroupGlobal(ctx *gin.Context) {\n\tvar pggOption model.Options\n\tdatabase.GetDB().Where(&model.Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)\n\n\tvar pgg model.PolicyGroup\n\terr := json.Unmarshal([]byte(pggOption.Value), &pgg)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, gin.H{\"data\": pgg})\n}\n\nfunc PutSrcIPConfig(ctx *gin.Context) {\n\tvar params model.SrcIPConfig\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tvar scOption model.Options\n\t\tres := tx.Where(&model.Options{Key: constants.SrcIPConfig}).First(&scOption)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tscStr, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\tlogger.Error(err)\n\t\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\t\treturn err\n\t\t}\n\n\t\tscOption.Value = string(scStr)\n\t\ttx.Save(&scOption)\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc GetSrcIPConfig(ctx *gin.Context) {\n\tsrcIPConfig, err := model.GetSrcIPConfig(database.GetDB().DB)\n\tif err != nil {\n\t\treturn\n\t}\n\tresponse.Success(ctx, gin.H{\"data\": srcIPConfig})\n}\n"
  },
  {
    "path": "management/webserver/api/policyrule.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"gorm.io/gorm\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\ntype putSwitchRequest struct {\n\tIDs       []uint `json:\"ids\"`\n\tIsEnabled bool   `json:\"is_enabled\"`\n}\n\nfunc PostPolicyRule(ctx *gin.Context) {\n\tvar params model.PolicyRule\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tpolicyRule := &model.PolicyRule{Action: params.Action, Comment: params.Comment, IsEnabled: params.IsEnabled, Pattern: params.Pattern}\n\t\tres := tx.Create(policyRule)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc PutSwitchPolicyRule(ctx *gin.Context) {\n\tvar params putSwitchRequest\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tres := tx.Model(&model.PolicyRule{}).Where(params.IDs).Updates(model.PolicyRule{IsEnabled: params.IsEnabled})\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc PutPolicyRule(ctx *gin.Context) {\n\tvar params model.PolicyRule\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tvar policyRule model.PolicyRule\n\t\tres := tx.Where(params.ID).First(&policyRule)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tpolicyRule.Action = params.Action\n\t\tpolicyRule.Comment = params.Comment\n\t\tpolicyRule.IsEnabled = params.IsEnabled\n\t\tpolicyRule.Pattern = params.Pattern\n\t\ttx.Save(&policyRule)\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc DeletePolicyRule(ctx *gin.Context) {\n\tvar params idsRequest\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tres := tx.Where(params.IDs).Delete(&model.PolicyRule{})\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tif err := fvm.PushFSL(tx); err != nil {\n\t\t\treturn errors.New(\"Rules compile error, please check your params.\")\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc GetPolicyRule(ctx *gin.Context) {\n\tvar params pageRequest\n\tif err := ctx.BindQuery(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\tvar policyRuleList []model.PolicyRule\n\tdb.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order(\"id desc\").Find(&policyRuleList)\n\n\tvar total int64\n\tdb.Model(&model.PolicyRule{}).Count(&total)\n\n\tresponse.Success(ctx, gin.H{\"data\": policyRuleList, \"total\": total})\n}\n"
  },
  {
    "path": "management/webserver/api/response/error.go",
    "content": "package response\n\nconst (\n\tErrLoginRequired = \"login-required\"\n\tErrWrongPasscode = \"wrong-passcode\"\n\tErrWrongTimeGap  = \"wrong-time-gap\"\n\tErrInternalError = \"internal-error\"\n\tErrDataNotExist  = \"data-not-exist\"\n\tErrWrongFileType = \"wrong-filetype\"\n\tErrReadOnly      = \"read-only\"\n)\n"
  },
  {
    "path": "management/webserver/api/response/jsonbody.go",
    "content": "package response\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype JSONBody struct {\n\tErr  string\n\tMsg  string\n\tData interface{}\n}\n\nvar (\n\tErrorLoginRequired = JSONBody{ErrLoginRequired, \"Login required\", nil}\n\tErrorParamNotOK    = JSONBody{ErrInternalError, \"Error occurred when extracting params\", nil}\n\tErrorDataNotExist  = JSONBody{ErrDataNotExist, \"Data queried does not exist\", nil}\n\tErrorReadOnly      = JSONBody{ErrReadOnly, \"This environment is read only\", nil}\n)\n\nfunc Success(c *gin.Context, data interface{}) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"data\": data,\n\t\t\"msg\":  \"\",\n\t\t\"err\":  nil,\n\t})\n\t//c.Abort()\n}\n\nfunc SuccessWithList(c *gin.Context, data interface{}) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"data\": data,\n\t\t\"msg\":  \"\",\n\t\t\"err\":  nil,\n\t})\n\t//c.Abort()\n}\n\nfunc SuccessWithMsg(c *gin.Context, data interface{}, msg string) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"data\": data,\n\t\t\"msg\":  msg,\n\t\t\"err\":  nil,\n\t})\n\t//c.Abort()\n}\n\nfunc Error(c *gin.Context, rsp JSONBody, status int) {\n\tc.JSON(status, gin.H{\n\t\t\"data\": rsp.Data,\n\t\t\"msg\":  rsp.Msg,\n\t\t\"err\":  rsp.Err,\n\t})\n\t//c.Abort()\n}\n"
  },
  {
    "path": "management/webserver/api/response/png.go",
    "content": "package response\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst StreamContentType = \"application/octet-stream\"\n\nfunc PNG(c *gin.Context, bytes []byte) {\n\tc.Data(http.StatusOK, StreamContentType, bytes)\n}\n"
  },
  {
    "path": "management/webserver/api/website.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"gorm.io/gorm\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/rpc\"\n)\n\nfunc publishWebsiteConfig(website *model.Website) error {\n\tbyteWebsite, err := json.Marshal(website)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = rpc.Publish(byteWebsite, rpc.EventTypeWebsite)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc publishDeleteWebsiteConfig(id []uint) error {\n\tbyteId, err := json.Marshal(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = rpc.Publish(byteId, rpc.EventTypeDeleteWebsite)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc PostWebsite(ctx *gin.Context) {\n\tvar params model.Website\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\twebsite := &model.Website{Comment: params.Comment, ServerNames: params.ServerNames, Upstreams: params.Upstreams, Ports: params.Ports,\n\t\t\tCertFilename: params.CertFilename, KeyFilename: params.KeyFilename, IsEnabled: true}\n\t\tres := tx.Create(website)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\n\t\tif config.GlobalConfig.Server.DevMode {\n\t\t\treturn nil\n\t\t}\n\n\t\terr := publishWebsiteConfig(website)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc PutWebsite(ctx *gin.Context) {\n\tvar params model.Website\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tvar website model.Website\n\t\tres := tx.Where(params.ID).First(&website)\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\twebsite.Comment = params.Comment\n\t\twebsite.ServerNames = params.ServerNames\n\t\twebsite.Upstreams = params.Upstreams\n\t\twebsite.Ports = params.Ports\n\t\twebsite.CertFilename = params.CertFilename\n\t\twebsite.KeyFilename = params.KeyFilename\n\t\twebsite.IsEnabled = true\n\t\ttx.Save(&website)\n\n\t\tif config.GlobalConfig.Server.DevMode {\n\t\t\treturn nil\n\t\t}\n\n\t\terr := publishWebsiteConfig(&params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc DeleteWebsite(ctx *gin.Context) {\n\tvar params idsRequest\n\tif err := ctx.BindJSON(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tres := tx.Where(params.IDs).Delete(&model.Website{})\n\t\tif res.Error != nil {\n\t\t\treturn res.Error\n\t\t}\n\t\tif res.RowsAffected == 0 {\n\t\t\treturn errors.New(\"Data queried does not exist\")\n\t\t}\n\n\t\tif config.GlobalConfig.Server.DevMode {\n\t\t\treturn nil\n\t\t}\n\n\t\terr := publishDeleteWebsiteConfig(params.IDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.JSONBody{Err: response.ErrInternalError, Msg: err.Error()}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresponse.Success(ctx, nil)\n}\n\nfunc GetWebsite(ctx *gin.Context) {\n\tvar params pageRequest\n\tif err := ctx.BindQuery(&params); err != nil {\n\t\tlogger.Error(err)\n\t\tresponse.Error(ctx, response.ErrorParamNotOK, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdb := database.GetDB()\n\ttype Website struct {\n\t\tmodel.Website\n\t\tReqValue    int64 `json:\"req_value\"`\n\t\tDeniedValue int64 `json:\"denied_value\"`\n\t}\n\tvar websiteList []Website\n\tdb.Limit(params.PageSize).Offset(params.PageSize * (params.Page - 1)).Order(\"id desc\").Find(&websiteList)\n\n\tvar statistics []model.SystemStatistics\n\tvar websiteIds []string\n\tfor _, i := range websiteList {\n\t\twebsiteIds = append(websiteIds, strconv.Itoa(int(i.ID)))\n\t}\n\tdb.Where(\"created_at >= date_trunc('day',now())\").Where(\"website in (?)\", websiteIds).Find(&statistics)\n\n\tfor i, website := range websiteList {\n\t\tfor _, j := range statistics {\n\t\t\tif strconv.Itoa(int(website.ID)) == j.Website {\n\t\t\t\tif j.Type == \"website-req\" {\n\t\t\t\t\twebsiteList[i].ReqValue = j.Value\n\t\t\t\t} else if j.Type == \"website-denied\" {\n\t\t\t\t\twebsiteList[i].DeniedValue = j.Value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar total int64\n\tdb.Model(&model.Website{}).Count(&total)\n\n\tresponse.Success(ctx, gin.H{\"data\": websiteList, \"total\": total})\n}\n"
  },
  {
    "path": "management/webserver/cmd/fake_logs.go",
    "content": "package cmd\n\nimport \"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\nfunc FakeLogs() {\n\tmodel.InitDetectLogSamples()\n}\n"
  },
  {
    "path": "management/webserver/cmd/gen_certs.go",
    "content": "package cmd\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"path/filepath\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\nfunc GenCerts() error {\n\tif err := genServerCert(); err != nil {\n\t\treturn err\n\t}\n\tif err := genClientCACert(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc genServerCert() error {\n\tcertPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, \"server.crt\")\n\tkeyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, \"server.key\")\n\tif err := utils.WriteCertIfNotExist(\n\t\tcertPath,\n\t\tkeyPath,\n\t\tfunc() ([]byte, []byte, error) {\n\t\t\treturn utils.GenerateCert(\n\t\t\t\t[]string{},\n\t\t\t\t3650,\n\t\t\t\t4096,\n\t\t\t\t&pkix.Name{\n\t\t\t\t\tCountry:            []string{\"CN\"},\n\t\t\t\t\tProvince:           []string{\"Beijing\"},\n\t\t\t\t\tLocality:           []string{\"Beijing\"},\n\t\t\t\t\tOrganization:       []string{\"Beijing WAF Technology Co., Ltd.\"},\n\t\t\t\t\tOrganizationalUnit: []string{\"Service Infrastructure Department\"},\n\t\t\t\t\tCommonName:         \"WAF Management Server\",\n\t\t\t\t},\n\t\t\t\tfalse,\n\t\t\t)\n\t\t}); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc genClientCACert() error {\n\tcertPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, \"client_ca.crt\")\n\tkeyPath := filepath.Join(config.GlobalConfig.MgtResDir, constants.CertsPath, \"client_ca.key\")\n\tif err := utils.WriteCertIfNotExist(\n\t\tcertPath,\n\t\tkeyPath,\n\t\tfunc() ([]byte, []byte, error) {\n\t\t\treturn utils.GenerateCert(\n\t\t\t\t[]string{},\n\t\t\t\t3650,\n\t\t\t\t4096,\n\t\t\t\t&pkix.Name{\n\t\t\t\t\tCountry:            []string{\"CN\"},\n\t\t\t\t\tProvince:           []string{\"Beijing\"},\n\t\t\t\t\tLocality:           []string{\"Beijing\"},\n\t\t\t\t\tOrganization:       []string{\"Beijing WAF Technology Co., Ltd.\"},\n\t\t\t\t\tOrganizationalUnit: []string{\"Service Infrastructure Department\"},\n\t\t\t\t\tCommonName:         \"WAF Client Certificate Authority\",\n\t\t\t\t},\n\t\t\t\ttrue,\n\t\t\t)\n\t\t}); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/cmd/push_fsl.go",
    "content": "package cmd\n\nimport (\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm\"\n)\n\nfunc PushFSL() error {\n\treturn fvm.PushFSL(database.GetDB().DB)\n}\n"
  },
  {
    "path": "management/webserver/cmd/reset_user.go",
    "content": "package cmd\n\nimport (\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\nfunc ResetUser(username string) {\n\tdb := database.GetDB()\n\tvar user model.User\n\tdb.Where(&model.User{Username: username}).First(&user)\n\tuser.LastLoginTime = 0\n\tdb.Save(&user)\n}\n"
  },
  {
    "path": "management/webserver/cmd/show_fsl.go",
    "content": "package cmd\n\nimport (\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm\"\n)\n\nfunc ShowFSL() (string, error) {\n\treturn fvm.GenerateFullFSL(database.GetDB().DB)\n}\n"
  },
  {
    "path": "management/webserver/config.yml",
    "content": "# develop use only. For production, refer to `package/build/mgt-api/webserver/config.yml`\nlog:\n  output: stdout              # \"stdout\", \"stderr\" or file path\n  level:  debug                # \"debug\", \"info\", \"warn\" or \"error\"\nserver:\n  listen_addr: :9001\n  dev_mode: true\ndb:\n  url: postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce\n  log_sql: false\ndetector:\n  addr: \"\"\n  fsl_bytecode: fvm/bytecode\ngrpc_server:\n  listen_addr: :9002\n\n"
  },
  {
    "path": "management/webserver/go.mod",
    "content": "module chaitin.cn/patronus/safeline-2/management/webserver\n\ngo 1.21\n\ntoolchain go1.21.3\n\nrequire (\n\tchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6\n\tchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c\n\tchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c\n\tgithub.com/gin-contrib/sessions v0.0.5\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/pquerna/otp v1.4.0\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/rogpeppe/go-internal v1.10.0\n\tgithub.com/sirupsen/logrus v1.4.2\n\tgoogle.golang.org/grpc v1.65.0\n\tgorm.io/datatypes v1.1.1\n\tgorm.io/driver/postgres v1.5.9\n\tgorm.io/gorm v1.25.10\n)\n\nrequire (\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/bytedance/sonic v1.11.6 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.20.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.7.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/gorilla/context v1.1.1 // indirect\n\tgithub.com/gorilla/securecookie v1.1.1 // indirect\n\tgithub.com/gorilla/sessions v1.2.1 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect\n\tgithub.com/jackc/pgx/v5 v5.5.5 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // indirect\n\tgithub.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgolang.org/x/arch v0.8.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/text v0.15.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgorm.io/driver/mysql v1.4.7 // indirect\n)\n"
  },
  {
    "path": "management/webserver/go.sum",
    "content": "chaitin.cn/dev/go/errors v0.0.0-20200717101723-df6132d53dc8/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=\nchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6 h1:1Qa9ABk907/9ZrOLbbRcS8Fqq9VhjAF/mLjbSP1qAJY=\nchaitin.cn/dev/go/errors v0.0.0-20210324055134-dc5247602af6/go.mod h1:u+ZD0shdyUt0UG9XfCgD1R5mSqXpTVoO5rwQXQeo3Eo=\nchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c h1:Xn9IYkxmnpDcEpV+7JIR5ufEIexd1dhqKwpOLG1mYOE=\nchaitin.cn/dev/go/log v0.0.0-20221220104336-05125760b10c/go.mod h1:xJIYwUoA2TX5mNg/RBrEPyE251BPwj+70/mM7UIhoxg=\nchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c h1:tXsraF7o9iUsQY6IwpDJusc6OFhB7iv/bBTfgR3MPUU=\nchaitin.cn/dev/go/settings v0.0.0-20221220104336-05125760b10c/go.mod h1:fUvtmpG8Z8Zf5aciadL9a/vn5SB3knG7pdNJixDplPg=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=\ngithub.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=\ngithub.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=\ngithub.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=\ngithub.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=\ngithub.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=\ngithub.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=\ngithub.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=\ngithub.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=\ngithub.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=\ngithub.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=\ngithub.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=\ngithub.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=\ngithub.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=\ngithub.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=\ngithub.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=\ngithub.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=\ngolang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=\ngolang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=\ngolang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=\ngoogle.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=\ngoogle.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=\ngoogle.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=\ngoogle.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/datatypes v1.1.1 h1:XAjO7NNfUKVUvnS3+BkqMrPXxCAcxDlpOYbjnizxNCw=\ngorm.io/datatypes v1.1.1/go.mod h1:u8GEgFjJ+GpsGfgHmBUcQqHm/937t3sj/SO9dvbndTg=\ngorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=\ngorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=\ngorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=\ngorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=\ngorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=\ngorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=\ngorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=\ngorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=\ngorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=\ngorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=\ngorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\n"
  },
  {
    "path": "management/webserver/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-contrib/sessions/cookie\"\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/cmd\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/middleware\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/cron\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/rpc\"\n)\n\nvar (\n\tlogger = log.GetLogger(\"main\")\n\n\tversion    = \"undefined\"\n\tgithash    = \"undefined\"\n\tbuildstamp = \"undefined\"\n\tgoVersion  = \"undefined\"\n)\n\nfunc init() {\n\t// do something that do not raise error\n}\n\nfunc main() {\n\tlog.SetLogFormatter()\n\n\tfs := flag.NewFlagSet(\"webserver\", flag.ExitOnError)\n\tshowVersion := fs.Bool(\"v\", false, \"show version\")\n\tcfgFile := fs.String(\"c\", constants.ConfigFilePath, \"config file path\")\n\tgenCerts := fs.Bool(\"gen_certs\", false, \"generate certs\")\n\tshowFSL := fs.Bool(\"show_fsl\", false, \"show full selectors\")\n\tpush_fsl := fs.Bool(\"push_fsl\", false, \"compile and push fsl\")\n\tfakeLogs := fs.Bool(\"fake_logs\", false, \"fake logs\")\n\tresetUsername := fs.String(\"reset_user\", \"\", \"reset user\")\n\tif err := fs.Parse(os.Args[1:]); err != nil {\n\t\tlogger.Fatalln(\"Failed to parse args: \", err)\n\t}\n\n\tif *showVersion {\n\t\ti, _ := strconv.Atoi(buildstamp)\n\t\tt := time.Unix(int64(i), 0).Format(\"2006-01-02 15:04:05\")\n\t\tfmt.Println(\"Version:    \", version)\n\t\tfmt.Println(\"Githash:    \", githash)\n\t\tfmt.Println(\"Build:      \", t)\n\t\tfmt.Println(\"Go version: \", goVersion)\n\t\treturn\n\t}\n\n\tconstants.Version = strings.TrimPrefix(version, \"ce-\")\n\t// init configs\n\tif err := config.InitConfigs(*cfgFile); err != nil {\n\t\tlogger.Fatalln(\"Failed to init configs: \", err)\n\t}\n\n\tif err := log.InitLogger(); err != nil {\n\t\tlogger.Fatalln(\"Failed to init db: \", err)\n\t}\n\n\tif *genCerts {\n\t\tif err := cmd.GenCerts(); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to generate certs: \", err)\n\t\t}\n\t\treturn\n\t}\n\n\tlogger.Info(\"Init database\")\n\tif err := database.InitDB(); err != nil {\n\t\tlogger.Fatalln(\"Failed to init db: \", err)\n\t}\n\n\tif *showFSL {\n\t\tif fullFSL, err := cmd.ShowFSL(); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to generate fsl: \", err)\n\t\t} else {\n\t\t\tlogger.Info(strings.ReplaceAll(strings.ReplaceAll(fullFSL, \";\", \";\\n\"), \"CREATE\", \"\\nCREATE\"))\n\t\t}\n\t\treturn\n\t}\n\n\tlogger.Info(\"Init models\")\n\tif err := model.InitModels(); err != nil {\n\t\tlogger.Fatalln(\"Failed to init models: \", err)\n\t}\n\n\tif len(*resetUsername) > 0 {\n\t\tlogger.Infoln(\"reset user:\", *resetUsername)\n\t\tcmd.ResetUser(*resetUsername)\n\t\tlogger.Infoln(\"success!\")\n\t\treturn\n\t}\n\n\tif *fakeLogs {\n\t\tlogger.Infoln(\"faking logs...\")\n\t\tcmd.FakeLogs()\n\t\tlogger.Infoln(\"success!\")\n\t\treturn\n\t}\n\n\tif *push_fsl {\n\t\tlogger.Infoln(\"push fsl...\")\n\t\tif err := cmd.PushFSL(); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to generate fsl: \", err)\n\t\t}\n\t\tlogger.Infoln(\"success!\")\n\t\treturn\n\t}\n\n\tlogger.Info(\"Init FVM bytecode\")\n\tfvm.InitFVMBytecode()\n\n\tif err := cron.StartCron(); err != nil {\n\t\tlogger.Fatalln(\"Failed to start cron: \", err)\n\t}\n\n\tif err := rpc.StartGRPCSever(); err != nil {\n\t\tlogger.Fatalln(\"Failed to start gRPC server: \", err)\n\t}\n\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.Default()\n\n\tvar option model.Options\n\tdatabase.GetDB().Where(&model.Options{Key: constants.SecretKey}).First(&option)\n\tlogger.Debugf(\"Secret: %s\", option.Value)\n\tstore := cookie.NewStore([]byte(option.Value))\n\tr.Use(sessions.Sessions(\"session\", store))\n\n\tpublicRouters := r.Group(\"/api\")\n\tpublicRouters.POST(api.Login, api.PostLogin)\n\tpublicRouters.POST(api.Logout, api.PostLogout)\n\tpublicRouters.POST(api.Behaviour, api.PostBehaviour)\n\tpublicRouters.POST(api.FalsePositives, api.PostFalsePositives)\n\tpublicRouters.GET(api.OTPUrl, api.GetOTPUrl)\n\tpublicRouters.GET(api.Version, api.GetVersion)\n\tpublicRouters.GET(api.UpgradeTips, api.GetUpgradeTips)\n\t// test use\n\tpublicRouters.GET(\"/Ping\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"message\": \"pong\",\n\t\t})\n\t})\n\n\tlimitedRouters := r.Group(\"/api\")\n\tnoAuth, existed := os.LookupEnv(\"NO_AUTH\")\n\tif existed && len(noAuth) >= 0 {\n\t\tlogger.Warn(\"No auth\")\n\t} else {\n\t\tlimitedRouters.Use(middleware.AuthRequired)\n\t}\n\treadOnly, existed := os.LookupEnv(\"READ_ONLY\")\n\tif existed && len(readOnly) >= 0 {\n\t\tlogger.Warn(\"Read only\")\n\t\tlimitedRouters.Use(middleware.ReadOnly)\n\t}\n\n\tlimitedRouters.GET(api.User, api.GetUser)\n\n\tlimitedRouters.GET(api.DetectLogList, api.GetDetectLogList)\n\tlimitedRouters.GET(api.DetectLogDetail, api.GetDetectLogDetail)\n\n\tlimitedRouters.POST(api.Website, api.PostWebsite)\n\tlimitedRouters.PUT(api.Website, api.PutWebsite)\n\tlimitedRouters.DELETE(api.Website, api.DeleteWebsite)\n\tlimitedRouters.GET(api.Website, api.GetWebsite)\n\n\tlimitedRouters.POST(api.UploadSSLCert, api.PostUploadSSLCert)\n\tlimitedRouters.POST(api.SSLCert, api.PostSSLCert)\n\n\tlimitedRouters.POST(api.PolicyRule, api.PostPolicyRule)\n\tlimitedRouters.PUT(api.PolicyRule, api.PutPolicyRule)\n\tlimitedRouters.DELETE(api.PolicyRule, api.DeletePolicyRule)\n\tlimitedRouters.GET(api.PolicyRule, api.GetPolicyRule)\n\tlimitedRouters.PUT(api.SwitchPolicyRule, api.PutSwitchPolicyRule)\n\n\t// 仪表盘接口\n\tlimitedRouters.GET(api.DashboardCounts, api.GetDashboardCounts)\n\tlimitedRouters.GET(api.DashboardSites, api.GetDashboardSites)\n\tlimitedRouters.GET(api.DashboardQps, api.GetDashboardQps)\n\tlimitedRouters.GET(api.DashboardRequests, api.GetDashboardRequests)\n\tlimitedRouters.GET(api.DashboardIntercepts, api.GetDashboardIntercepts)\n\n\tlimitedRouters.GET(api.PolicyGroupGlobal, api.GetPolicyGroupGlobal)\n\tlimitedRouters.PUT(api.PolicyGroupGlobal, api.PutPolicyGroupGlobal)\n\n\tlimitedRouters.GET(api.SrcIPConfig, api.GetSrcIPConfig)\n\tlimitedRouters.PUT(api.SrcIPConfig, api.PutSrcIPConfig)\n\n\tlogger.Info(\"Staring...\")\n\tif err := r.Run(config.GlobalConfig.Server.ListenAddr); err != nil {\n\t\tlogger.Fatalln(\"Error occurred when running web server: \", err)\n\t}\n}\n"
  },
  {
    "path": "management/webserver/middleware/auth.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/api/response\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n)\n\nfunc AuthRequired(c *gin.Context) {\n\tsession := sessions.Default(c)\n\n\tuser := session.Get(constants.DefaultSessionUserKey)\n\tif user == nil {\n\t\tresponse.Error(c, response.ErrorLoginRequired, http.StatusUnauthorized)\n\t\tc.Abort()\n\t\treturn\n\t}\n\n\t// extend session expired time\n\tsession.Options(sessions.Options{\n\t\tPath:   \"/\",\n\t\tMaxAge: 3600 * 24 * 7,\n\t\t//Domain: options.Domain,\n\t\t//HttpOnly: true,\n\t\t//SameSite: http.SameSiteLaxMode,\n\t\t//Secure:   false,\n\t})\n\n\tif err := session.Save(); err != nil {\n\t\tresponse.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: \"Error occurred when creating sessions\"}, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tc.Next()\n}\n\nfunc ReadOnly(c *gin.Context) {\n\tif c.Request.Method != \"GET\" && c.Request.Method != \"HEAD\" && c.Request.Method != \"OPTIONS\" {\n\t\tresponse.Error(c, response.ErrorReadOnly, http.StatusBadRequest)\n\t\tc.Abort()\n\t}\n}\n"
  },
  {
    "path": "management/webserver/model/base.go",
    "content": "package model\n\nimport (\n\t\"time\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log\"\n)\n\n// Base is a replacement for gorm.Model without DeletedAt, which is considered to be not good.\ntype Base struct {\n\tID        uint      `gorm:\"primarykey\"  json:\"id\"`\n\tCreatedAt time.Time `                   json:\"created_at\"`\n\tUpdatedAt time.Time `                   json:\"updated_at\"`\n}\n\nvar logger = log.GetLogger(\"model\")\n"
  },
  {
    "path": "management/webserver/model/behaviour.go",
    "content": "package model\n\ntype Behaviour struct {\n\tBase\n\tSrcRouter string `json:\"src_router\"`\n\tDstRouter string `json:\"dst_router\"`\n}\n"
  },
  {
    "path": "management/webserver/model/db_patch_1_4_0.go",
    "content": "package model\n\nimport (\n\t\"strings\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"gorm.io/gorm\"\n)\n\ntype sqlResult struct {\n\tIds string `json:\"ids\"`\n}\n\nfunc DBPatch140(tx *gorm.DB) error {\n\tif !tx.Migrator().HasTable(&SystemStatistics{}) {\n\t\treturn nil\n\t}\n\n\tvar result []sqlResult\n\t//SELECT string_agg(id::text, ',') as ids FROM mgt_system_statistics GROUP BY (type, website, created_at) HAVING COUNT(*) > 1) as tmp\n\tres := tx.Model(&SystemStatistics{}).Select(\"string_agg(id::text, ',') as ids\").Group(\"type, website, created_at\").Having(\"COUNT(*) > 1\").Find(&result)\n\tif res.Error != nil {\n\t\treturn errors.Wrap(res.Error, \"Failed to select data\")\n\t}\n\n\tif len(result) <= 0 {\n\t\treturn nil\n\t}\n\n\tvar deleteIds []string\n\tfor _, s := range result {\n\t\ttmpIds := strings.Split(s.Ids, \",\")\n\t\tdeleteIds = append(deleteIds, tmpIds...)\n\t}\n\tdeleteRes := tx.Delete(&SystemStatistics{}, deleteIds)\n\n\treturn errors.Wrap(deleteRes.Error, \"Failed to delete\")\n}\n"
  },
  {
    "path": "management/webserver/model/detectlog.go",
    "content": "package model\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\n// DetectLog is designed to be used in response, not a good naming.\ntype DetectLog struct {\n\tDetectLogBasic\n\tDetectLogDetail\n\tEventId    string `json:\"event_id\"` // to eliminate ambiguous\n\tWebsite    string `json:\"website\"`\n\tAttackType string `json:\"attack_type\"`\n\tModule     string `json:\"module\"`\n\tReason     string `json:\"reason\"`\n}\n\nfunc getRuleModule(ruleId string, attackType int) string {\n\tvar module string\n\tif strings.HasPrefix(ruleId, \"m_rule\") { // `m_rule/65543`\n\t\tmodule = \"m_rule\"\n\t} else if strings.HasPrefix(ruleId, \"/\") {\n\t\tif attackType == -2 {\n\t\t\tmodule = \"whitelist\"\n\t\t} else { //  if attackType == -3\n\t\t\tmodule = \"blacklist\"\n\t\t}\n\t} else {\n\t\tmodule = ruleId\n\t}\n\treturn constants.RuleModule[module]\n}\n\nfunc getRuleReason(ruleId string, attackType int) string {\n\tvar reason string\n\tif strings.HasPrefix(ruleId, \"m_rule\") { // `m_rule/65543`\n\t\treason = constants.RuleReason[strings.ReplaceAll(ruleId, \"m_rule/\", \"\")]\n\t} else if strings.HasPrefix(ruleId, \"/\") {\n\t\treason = ruleId[1:]\n\t} else {\n\t\treason = fmt.Sprintf(\"检测到 %s 攻击\", getAttackType(attackType))\n\t}\n\treturn reason\n}\n\nfunc getAttackType(at int) string {\n\tatStr, ok := constants.AttackType[at]\n\tif !ok {\n\t\tatStr = constants.AttackType[62] // unknown\n\t}\n\treturn atStr\n}\n\nfunc getCountry(country string) string {\n\tcountryStr, ok := constants.CountryCode[country]\n\tif !ok {\n\t\tcountryStr = \"\"\n\t}\n\treturn countryStr\n}\n\nfunc TransformDetectLog(basic *DetectLogBasic, detail *DetectLogDetail) (*DetectLog, error) {\n\tif basic == nil {\n\t\treturn nil, errors.New(\"basic *DetectLogBasic cannot be nil\")\n\t}\n\tif detail != nil && basic.EventId != detail.EventId {\n\t\treturn nil, errors.New(\"EventId field should be the same for basic and detail\")\n\t}\n\n\tbasic.Country = getCountry(basic.Country)\n\tdLog := DetectLog{\n\t\tDetectLogBasic: *basic,\n\t\tEventId:        basic.EventId,\n\t\tWebsite:        utils.BuildUrl(basic.Protocol, basic.Host, basic.DstPort, basic.UrlPath),\n\t\tAttackType:     getAttackType(basic.AttackType),\n\t\tModule:         getRuleModule(basic.RuleId, basic.AttackType),\n\t\tReason:         getRuleReason(basic.RuleId, basic.AttackType),\n\t}\n\tif detail != nil {\n\t\tdLog.DetectLogDetail = *detail\n\t}\n\n\treturn &dLog, nil\n}\n\ntype DetectLogBasic struct {\n\tID      uint   `json:\"id\"               gorm:\"primarykey\"`\n\tEventId string `json:\"event_id\"         gorm:\"uniqueIndex;not null\"`\n\n\tSiteUUID string `json:\"site_uuid\"       gorm:\"column:site_uuid\"`\n\tSrcIp    string `json:\"src_ip\"          gorm:\"index\"`\n\tSocketIp string `json:\"socket_ip\"       gorm:\"index\"`\n\n\tProtocol int    `json:\"protocol\"`\n\tHost     string `json:\"host\"`\n\tUrlPath  string `json:\"url_path\"`\n\tDstPort  uint   `json:\"dst_port\"`\n\n\tCountry  string `json:\"country\"`\n\tProvince string `json:\"province\"`\n\tCity     string `json:\"city\"`\n\n\tAttackType int    `json:\"attack_type\"   gorm:\"index\"`\n\tRiskLevel  int    `json:\"risk_level\"    gorm:\"index\"`\n\tAction     int    `json:\"action\"        gorm:\"index\"`\n\tRuleId     string `json:\"rule_id\"`\n\n\tTimestamp int64 `json:\"timestamp\"       gorm:\"index\"`\n}\n\ntype DetectLogDetail struct {\n\tID      uint   `                        gorm:\"primarykey\"`\n\tEventId string `                        gorm:\"uniqueIndex;not null\"`\n\n\tSrcPort uint   `json:\"src_port\"`\n\tDstIp   string `json:\"dst_ip\"`\n\n\tMethod      string `json:\"method\"`\n\tQueryString string `json:\"query_string\"`\n\tStatusCode  uint   `json:\"status_code\"`\n\n\tReqHeader string `json:\"req_header\"`\n\tReqBody   string `json:\"req_body\"`\n\tRspHeader string `json:\"rsp_header\"`\n\tRspBody   string `json:\"rsp_body\"`\n\n\tPayload    string `json:\"payload\"`\n\tLocation   string `json:\"location\"`\n\tDecodePath string `json:\"decode_path\"`\n}\n\nfunc InitDetectLogSamples() {\n\tdb := database.GetDB()\n\tvar detectLogBasicList []DetectLogBasic\n\tvar detectLogDetailList []DetectLogDetail\n\n\ttimestamp := time.Now().Unix()\n\tdetail := DetectLogDetail{\n\t\tSrcPort:     58694,\n\t\tDstIp:       \"10.2.35.143\",\n\t\tMethod:      \"GET\",\n\t\tQueryString: \"\",\n\t\tReqHeader:   \"GET /webshell.php HTTP/1.1\\nUser-Agent: curl/7.77.0\\nAccept: */*\\n\\n\\\"\",\n\t\tReqBody:     \"\",\n\t\tRspHeader:   \"\",\n\t\tRspBody:     \"\",\n\t\tPayload:     \"\",\n\t\tLocation:    \"urlpath\",\n\t\tDecodePath:  \"\",\n\t}\n\n\tprotocolList := []int{constants.ProtocolHTTP, constants.ProtocolHTTPS}\n\tportList := []uint{80, 443}\n\tprovinceList := []string{}\n\tcityList := []string{}\n\tipList := []string{}\n\truleIdList := []string{}\n\n\tfor i := 0; i < 100; i++ {\n\t\trandInt := rand.Intn(1000)\n\t\teventId := utils.RandStr(32)\n\n\t\tbasic := DetectLogBasic{}\n\t\tbasic.EventId = eventId\n\t\tbasic.SrcIp = ipList[randInt%len(ipList)]\n\t\tbasic.SocketIp = ipList[randInt%len(ipList)]\n\t\tbasic.Protocol = protocolList[randInt%len(protocolList)]\n\t\tbasic.DstPort = portList[randInt%len(portList)]\n\t\tbasic.Host = fmt.Sprintf(\"%s.com\", utils.RandStr(5))\n\t\tbasic.UrlPath = fmt.Sprintf(\"/%s\", utils.RandStr(10))\n\t\tbasic.Country = \"CN\"\n\t\tbasic.Province = provinceList[randInt%len(provinceList)]\n\t\tbasic.City = cityList[randInt%len(cityList)]\n\t\tbasic.AttackType = randInt % 32\n\t\tbasic.RiskLevel = randInt % 4\n\t\tbasic.Action = randInt % 2\n\t\tbasic.RuleId = ruleIdList[randInt%len(ruleIdList)]\n\t\tbasic.Timestamp = timestamp - int64(randInt*100)\n\n\t\tdetailCopy := detail\n\t\tdetailCopy.EventId = eventId\n\t\tdetectLogBasicList = append(detectLogBasicList, basic)\n\t\tdetectLogDetailList = append(detectLogDetailList, detailCopy)\n\t}\n\tdb.CreateInBatches(detectLogBasicList, 100)\n\tdb.CreateInBatches(detectLogDetailList, 100)\n}\n"
  },
  {
    "path": "management/webserver/model/init.go",
    "content": "package model\n\nimport (\n\t\"gorm.io/gorm\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\nfunc InitModels() error {\n\tdb := database.GetDB()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\t//\n\t\tif err := DBPatch140(tx); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := db.AutoMigrate(&User{}, &DetectLogBasic{}, &DetectLogDetail{}, &Behaviour{}, &Options{}, &Website{}, &PolicyRule{}, &SystemStatistics{}); err != nil {\n\t\treturn err\n\t}\n\n\tif err := initAdminUser(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := initOptions(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := initPolicyGroupGlobal(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := initSrcIPConfig(); err != nil {\n\t\treturn err\n\t}\n\n\t//InitDetectLogSamples()\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/model/option.go",
    "content": "package model\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"gorm.io/gorm/clause\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\ntype Options struct {\n\tBase\n\tKey   string `gorm:\"column:key;uniqueIndex\"`\n\tValue string `gorm:\"column:value;\"`\n}\n\nfunc initOptions() error {\n\tdb := database.GetDB()\n\n\tsecretKey := Options{Key: constants.SecretKey, Value: utils.RandStr(32)}\n\tdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&secretKey)\n\n\tmachineId := Options{Key: constants.MachineID, Value: utils.RandStr(32)}\n\t_ = db.Clauses(clause.OnConflict{DoNothing: true}).Create(&machineId)\n\tgo NotifyInstallation(machineId.Value)\n\n\treturn nil\n}\n\nfunc NotifyInstallation(machineId string) {\n\tlogger.Info(\"Notify installation\")\n\ttr := pkg.TelemetryRequest{\n\t\tTelemetry: pkg.TelemetryInfo{\n\t\t\tId: constants.TelemetryId,\n\t\t},\n\t\tSafeline: pkg.SafelineInfo{\n\t\t\tId:      machineId,\n\t\t\tType:    constants.Installation,\n\t\t\tVersion: constants.Version,\n\t\t},\n\t}\n\tdata, err := json.Marshal(tr)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\treader := bytes.NewReader(data)\n\trsp, err := pkg.DoPostTelemetry(utils.GetHTTPClient(), config.GlobalConfig.Telemetry.Addr, reader)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\tif rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusCreated {\n\t\tlogger.Errorf(\"transfer telemetry %s failed, status code = %d\", constants.Installation, rsp.StatusCode)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "management/webserver/model/policygroup.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\ntype PolicyGroup map[string]string\n\nconst (\n\tStrictMode  = \"strict\"\n\tDefaultMode = \"default\"\n\tDisableMode = \"disable\"\n\n\tSocketIP   = \"socket_ip\"\n\tHTTPHeader = \"http_header\"\n)\n\ntype SrcIPConfig struct {\n\tSource string `json:\"source\"` // socket_ip, http_header\n\tValue  string `json:\"value\"`\n}\n\ntype SkynetConfigModule struct {\n\tDetectConfig        interface{} `json:\"detect_config,omitempty\"`\n\tHighRiskAction      string      `json:\"high_risk_action\"`\n\tMediumRiskAction    string      `json:\"medium_risk_action\"`\n\tLowRiskAction       string      `json:\"low_risk_action\"`\n\tHighRiskEnableLog   int         `json:\"high_risk_enable_log\"`\n\tMediumRiskEnableLog int         `json:\"medium_risk_enable_log\"`\n\tLowRiskEnableLog    int         `json:\"low_risk_enable_log\"`\n\tState               string      `json:\"state\"`\n}\n\ntype SkynetConfig struct {\n\tDetectConfig string      `json:\"detect_config\"`\n\tDeepDetect   bool        `json:\"deep_detect\"`\n\tTimeout      interface{} `json:\"timeout\"`\n\n\tModules map[string]*SkynetConfigModule `json:\"modules\"`\n}\n\nfunc GetSrcIPConfig(db *gorm.DB) (*SrcIPConfig, error) {\n\tvar scOption Options\n\tdb.Where(&Options{Key: constants.SrcIPConfig}).First(&scOption)\n\n\tvar sc SrcIPConfig\n\terr := json.Unmarshal([]byte(scOption.Value), &sc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &sc, nil\n}\n\nfunc initSrcIPConfig() error {\n\tdb := database.GetDB()\n\n\t// policyGroupGlobal config, three mode: strict/default/disable\n\tvar srcIPConfig = SrcIPConfig{\n\t\tSource: SocketIP,\n\t\tValue:  \"\",\n\t}\n\n\tscStr, err := json.Marshal(srcIPConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpgg := Options{Key: constants.SrcIPConfig, Value: string(scStr)}\n\tdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&pgg)\n\n\treturn nil\n}\n\nfunc GetSkynetConfig(db *gorm.DB) (string, error) {\n\t// skynetConfigStr skynet config in 5.3.9\n\tconst skynetConfigStr = \"{\\\"decode_config\\\":{\\\"decode_methods\\\":[\\\"url decode\\\",\\\"JSON\\\",\\\"base64\\\",\\\"hex\\\",\\\"eval\\\",\\\"XML\\\",\\\"PHP deserialize\\\",\\\"utf7\\\"]},\\\"deep_detect\\\":false,\\\"modules\\\":{\\\"m_asp_code_injection\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_cmd_injection\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_csrf\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_file_include\\\":{\\\"detect_config\\\":{\\\"detect_suspicious_schema\\\":true},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_file_upload\\\":{\\\"detect_config\\\":{\\\"action_of_handling_custome_file\\\":[\\\"deny transmission\\\"],\\\"detect_file_content\\\":true,\\\"detect_real_file_content\\\":false,\\\"enable_module_in_detecting_file_content\\\":[\\\"java\\\",\\\"asp_code_injection\\\",\\\"php_code_injection\\\"],\\\"file_type_of_custome_action\\\":[],\\\"forbidden_extra_token\\\":false,\\\"forbidden_multiple_filename\\\":true,\\\"forbidden_suspicious_filename\\\":true},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_http\\\":{\\\"detect_config\\\":{\\\"warning_http_parse_failed\\\":false,\\\"warning_suspicious_http_version\\\":false},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"disabled\\\"},\\\"m_java\\\":{\\\"detect_config\\\":{\\\"detect_lookup\\\":false},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_java_unserialize\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_php_code_injection\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_php_unserialize\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_response\\\":{\\\"detect_config\\\":{\\\"detection_configure\\\":[\\\"detect_jsp_code_leak\\\",\\\"detect_php_code_leak\\\",\\\"detect_webshell\\\"],\\\"error_types\\\":[\\\"directory indexing\\\",\\\"SQL execution error\\\",\\\"server exception\\\"]},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"disabled\\\"},\\\"m_rule\\\":{\\\"detect_config\\\":{\\\"check_info_leak_by_rsp\\\":false,\\\"compatibility_mode\\\":false,\\\"detection_configure\\\":[\\\"detect_phpinfo\\\",\\\"detect_admin_page\\\",\\\"detect_backdoor\\\",\\\"detect_php_vuln\\\",\\\"detect_nginx\\\",\\\"detect_dedecms\\\",\\\"detect_apache\\\",\\\"detect_xml\\\",\\\"detect_struts2\\\",\\\"detect_java_vuln\\\",\\\"detect_php168\\\",\\\"detect_wordpress\\\",\\\"detect_directory_traversal\\\",\\\"detect_iis\\\",\\\"detect_gogs_gitea\\\",\\\"detect_thinkphp\\\",\\\"detect_jenkins\\\",\\\"detect_ecshop\\\",\\\"detect_nexus\\\",\\\"detect_drupal\\\",\\\"detect_ghostscript\\\",\\\"detect_atlassian\\\",\\\"detect_weblogic\\\",\\\"detect_coremail\\\",\\\"detect_phpcms\\\",\\\"detect_spring\\\",\\\"detect_southidc\\\",\\\"detect_fastjson\\\",\\\"detect_tbk_dvr\\\",\\\"detect_joomla\\\",\\\"detect_ecology_oa\\\",\\\"detect_jackson\\\",\\\"detect_xstream\\\",\\\"detect_activemq\\\",\\\"detect_solr\\\",\\\"detect_csii\\\",\\\"detect_big_ip\\\",\\\"detect_apisix\\\",\\\"detect_druid\\\",\\\"detect_log4j\\\"],\\\"info_leak_types\\\":[\\\"test file\\\",\\\"backup file\\\",\\\"code repository\\\",\\\"server sensitive file\\\"],\\\"rules_config\\\":{\\\"disable_ruleid\\\":[],\\\"enable_ruleid\\\":[],\\\"rules_status\\\":[]}},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_scanner\\\":{\\\"detect_config\\\":{\\\"language_types\\\":[\\\"python_scanner\\\",\\\"go_scanner\\\"],\\\"other_types\\\":[\\\"normal_scanner\\\"],\\\"spider_types\\\":[]},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"disabled\\\"},\\\"m_sqli\\\":{\\\"detect_config\\\":{\\\"detect_non_injection_sql\\\":true},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_ssrf\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_ssti\\\":{\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"},\\\"m_xss\\\":{\\\"detect_config\\\":{\\\"detect_complete_html\\\":true},\\\"high_risk_action\\\":\\\"deny\\\",\\\"high_risk_enable_log\\\":1,\\\"low_risk_action\\\":\\\"continue\\\",\\\"low_risk_enable_log\\\":1,\\\"medium_risk_action\\\":\\\"continue\\\",\\\"medium_risk_enable_log\\\":1,\\\"state\\\":\\\"enabled\\\"}},\\\"timeout\\\":{\\\"threshold\\\":1000,\\\"log\\\":\\\"enabled\\\"}}\"\n\tvar sc SkynetConfig\n\terr := json.Unmarshal([]byte(skynetConfigStr), &sc)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar pggOption Options\n\tdb.Where(&Options{Key: constants.PolicyGroupGlobal}).First(&pggOption)\n\n\tvar pgg PolicyGroup\n\terr = json.Unmarshal([]byte(pggOption.Value), &pgg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor module, mode := range pgg {\n\t\tswitch mode {\n\t\tcase StrictMode:\n\t\t\tscm := sc.Modules[module]\n\t\t\tscm.HighRiskAction = \"deny\"\n\t\t\tscm.MediumRiskAction = \"deny\"\n\t\t\tscm.LowRiskAction = \"deny\"\n\t\t\tscm.State = \"enabled\"\n\t\t\tsc.Modules[module] = scm\n\t\tcase DefaultMode:\n\t\t\tscm := sc.Modules[module]\n\t\t\tscm.HighRiskAction = \"deny\"\n\t\t\tscm.MediumRiskAction = \"continue\"\n\t\t\tscm.LowRiskAction = \"continue\"\n\t\t\tscm.State = \"enabled\"\n\t\t\tsc.Modules[module] = scm\n\t\tcase DisableMode:\n\t\t\tscm := sc.Modules[module]\n\t\t\tscm.HighRiskAction = \"continue\"\n\t\t\tscm.MediumRiskAction = \"continue\"\n\t\t\tscm.LowRiskAction = \"continue\"\n\t\t\tscm.State = \"disabled\"\n\t\t\tsc.Modules[module] = scm\n\t\tdefault:\n\t\t\treturn \"\", errors.New(\"no such mode\")\n\t\t}\n\t}\n\n\tscStr, err := json.Marshal(sc)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(scStr), nil\n}\n\nfunc initPolicyGroupGlobal() error {\n\tdb := database.GetDB()\n\n\t// policyGroupGlobal config, three mode: strict/default/disable\n\tvar policyGroupGlobal = PolicyGroup{\n\t\t\"m_asp_code_injection\": DefaultMode,\n\t\t\"m_cmd_injection\":      DefaultMode,\n\t\t\"m_csrf\":               DefaultMode,\n\t\t\"m_file_include\":       DefaultMode,\n\t\t\"m_file_upload\":        DefaultMode,\n\t\t\"m_http\":               DefaultMode,\n\t\t\"m_java\":               DefaultMode,\n\t\t\"m_java_unserialize\":   DefaultMode,\n\t\t\"m_php_code_injection\": DefaultMode,\n\t\t\"m_php_unserialize\":    DefaultMode,\n\t\t\"m_response\":           DefaultMode,\n\t\t\"m_rule\":               DefaultMode,\n\t\t\"m_scanner\":            DefaultMode,\n\t\t\"m_sqli\":               DefaultMode,\n\t\t\"m_ssrf\":               DefaultMode,\n\t\t\"m_ssti\":               DefaultMode,\n\t\t\"m_xss\":                DefaultMode,\n\t}\n\n\tpggStr, err := json.Marshal(policyGroupGlobal)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpgg := Options{Key: constants.PolicyGroupGlobal, Value: string(pggStr)}\n\tdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&pgg)\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/model/policyrule.go",
    "content": "package model\n\nimport \"gorm.io/datatypes\"\n\ntype PolicyRule struct {\n\tBase\n\tAction    int            `gorm:\"action\"                     json:\"action\"`\n\tComment   string         `gorm:\"comment\"                    json:\"comment\"`\n\tPattern   datatypes.JSON `gorm:\"pattern\"                    json:\"pattern\"`\n\tIsEnabled bool           `gorm:\"is_enabled;default=true\"    json:\"is_enabled\"`\n}\n\ntype PolicyRulePattern struct {\n\tK  string `json:\"k\"`\n\tOp string `json:\"op\"`\n\tV  string `json:\"v\"`\n}\n\nconst (\n\tKeySrcIP = \"src_ip\"\n\tKeyURI   = \"uri\"\n\tKeyHost  = \"host\"\n\n\tOpEq     = \"eq\"     // 完全相等\n\tOpMatch  = \"match\"  // 模糊匹配\n\tOpCIDR   = \"cidr\"   // CIDR\n\tOpHas    = \"has\"    // 关键字\n\tOpPrefix = \"prefix\" // 前缀关键字\n\tOpRe     = \"re\"     // 正则\n)\n"
  },
  {
    "path": "management/webserver/model/statistics.go",
    "content": "package model\n\nimport \"time\"\n\ntype SystemStatistics struct {\n\tID        uint      `json:\"id\"                gorm:\"primarykey\"`\n\tType      string    `json:\"type\"              gorm:\"index;uniqueIndex:type_website_createdat\"`\n\tValue     int64     `json:\"value\"`\n\tCreatedAt time.Time `json:\"created_at\"        gorm:\"index;uniqueIndex:type_website_createdat\"`\n\tWebsite   string    `json:\"website\"           gorm:\"index;uniqueIndex:type_website_createdat\"`\n}\n"
  },
  {
    "path": "management/webserver/model/user.go",
    "content": "package model\n\nimport (\n\t\"gorm.io/gorm/clause\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n)\n\ntype User struct {\n\tBase\n\tUsername string `gorm:\"uniqueIndex;not null\"`\n\tPassword string\n\tComment  string\n\n\tTFAEnabled    bool   `gorm:\"column:tfa_enabled;default:true\"`\n\tTFASecret     string `gorm:\"column:tfa_secret\"`\n\tLastLoginTime int64  `gorm:\"default:0\"`\n\tIsEnabled     bool   `gorm:\"default:true\"`\n}\n\nfunc initAdminUser() error {\n\tdb := database.GetDB()\n\tuser := User{\n\t\tUsername: constants.SuperUser,\n\t}\n\tdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/model/website.go",
    "content": "package model\n\nimport \"gorm.io/datatypes\"\n\ntype Website struct {\n\tBase\n\tComment     string         `gorm:\"comment\"           json:\"comment\"`\n\tServerNames datatypes.JSON `gorm:\"server_names\"      json:\"server_names\"`\n\tPorts       datatypes.JSON `gorm:\"ports\"             json:\"ports\"`\n\tUpstreams   datatypes.JSON `gorm:\"upstreams\"         json:\"upstreams\"`\n\n\tCertFilename string `gorm:\"cert_filename\"    json:\"cert_filename\"`\n\tKeyFilename  string `gorm:\"key_filename\"     json:\"key_filename\"`\n\n\tIsEnabled bool `gorm:\"is_enabled;default=true\"       json:\"is_enabled\"`\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\n\t\"chaitin.cn/dev/go/settings\"\n)\n\nvar (\n\tGlobalConfig = DefaultGlobalConfig()\n)\n\nfunc InitConfigs(configFilePath string) error {\n\ts, err := settings.New(configFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.DB.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.Log.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.Server.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.Detector.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.Telemetry.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err = GlobalConfig.GPRC.Load(s); err != nil {\n\t\treturn err\n\t}\n\n\tif err := settings.Unmarshal(\"platform_addr\", &GlobalConfig.PlatformAddr); err != nil {\n\t\treturn err\n\t}\n\n\tif v, ok := os.LookupEnv(\"MANAGEMENT_RESOURCES_DIR\"); ok {\n\t\tGlobalConfig.MgtResDir = v\n\t}\n\n\tif v, ok := os.LookupEnv(\"NGINX_RESOURCES_DIR\"); ok {\n\t\tGlobalConfig.NgxResDir = v\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/db.go",
    "content": "package config\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype DBConfig struct {\n\tURL     string `yaml:\"url\"`\n\tLogSQL  bool   `yaml:\"log_sql\"`\n\tSSLMode bool   `yaml:\"ssl_mode\"`\n}\n\nfunc DefaultDBConfig() DBConfig {\n\treturn DBConfig{\n\t\tURL:     \"postgres://safeline-ce:safeline-ce@127.0.0.1/safeline-ce\",\n\t\tLogSQL:  false,\n\t\tSSLMode: false,\n\t}\n}\n\nfunc (dbc *DBConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"db\", dbc); err != nil {\n\t\treturn err\n\t}\n\n\tif v, ok := os.LookupEnv(\"DATABASE_URL\"); ok {\n\t\tdbc.URL = v\n\t}\n\n\tdbURL, err := url.Parse(dbc.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tq := dbURL.Query()\n\tif !dbc.SSLMode {\n\t\tq.Set(\"sslmode\", \"disable\")\n\t}\n\tdbURL.RawQuery = q.Encode()\n\tdbc.URL = dbURL.String()\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/detector.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype DetectorConfig struct {\n\tAddr        string `yaml:\"addr\"`\n\tFslBytecode string `yaml:\"fsl_bytecode\"`\n}\n\nfunc DefaultDetectorConfig() DetectorConfig {\n\treturn DetectorConfig{\n\t\tAddr:        \"http://127.0.0.1:8001\",\n\t\tFslBytecode: \"bytecode\",\n\t}\n}\n\nfunc (d *DetectorConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"detector\", d); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/global.go",
    "content": "package config\n\ntype Config struct {\n\tLog          LogConfig\n\tDB           DBConfig\n\tServer       ServerConfig\n\tDetector     DetectorConfig\n\tTelemetry    TelemetryConfig\n\tGPRC         GRPCConfig\n\tPlatformAddr string\n\tMgtResDir    string\n\tNgxResDir    string\n}\n\nfunc DefaultGlobalConfig() Config {\n\treturn Config{\n\t\tLog:          DefaultLogConfig(),\n\t\tDB:           DefaultDBConfig(),\n\t\tServer:       DefaultServerConfig(),\n\t\tDetector:     DefaultDetectorConfig(),\n\t\tTelemetry:    DefaultTelemetryConfig(),\n\t\tGPRC:         DefaultGRPCConfig(),\n\t\tPlatformAddr: \"https://waf-ce.chaitin.cn\",\n\t\tMgtResDir:    \"/resources/management\",\n\t\tNgxResDir:    \"/resources/nginx\",\n\t}\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/grpc.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype GRPCConfig struct {\n\tListenAddr string `yaml:\"listen_addr\"`\n}\n\nfunc DefaultGRPCConfig() GRPCConfig {\n\treturn GRPCConfig{\n\t\tListenAddr: \":9002\",\n\t}\n}\n\nfunc (sc *GRPCConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"grpc_server\", sc); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/log.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype LogConfig struct {\n\tOutput string `yaml:\"output\"`\n\tLevel  string `yaml:\"level\"`\n}\n\nfunc DefaultLogConfig() LogConfig {\n\treturn LogConfig{\n\t\tOutput: \"stdout\",\n\t\tLevel:  \"info\",\n\t}\n}\n\nfunc (lc *LogConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"log\", lc); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/server.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype ServerConfig struct {\n\tListenAddr  string `yaml:\"listen_addr\"`\n\tDevMode     bool   `yaml:\"dev_mode\"`\n\tIntenseMode bool   `yaml:\"intense_mode\"`\n}\n\nfunc DefaultServerConfig() ServerConfig {\n\treturn ServerConfig{\n\t\tListenAddr: \":9001\",\n\t\tDevMode:    false,\n\t}\n}\n\nfunc (sc *ServerConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"server\", sc); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/config/telemetry.go",
    "content": "package config\n\nimport (\n\t\"chaitin.cn/dev/go/settings\"\n)\n\ntype TelemetryConfig struct {\n\tAddr string `yaml:\"addr\"`\n}\n\nfunc DefaultTelemetryConfig() TelemetryConfig {\n\treturn TelemetryConfig{\n\t\tAddr: \"rivers-telemetry:10086\",\n\t}\n}\n\nfunc (t *TelemetryConfig) Load(setting *settings.Setting) error {\n\tif err := setting.Unmarshal(\"telemetry\", t); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/constants/constants.go",
    "content": "package constants\n\nconst (\n\tSuperUser      = \"admin\"\n\tProductName    = \"长亭雷池 WAF 社区版\"\n\tProductVersion = \"\"\n\tConfigFilePath = \"config.yml\"\n\tCertsPath      = \"certs\"\n)\n\nconst (\n\tNotUpgrade int = iota\n\tRecommendedUpgrade\n\tMustUpgrade\n)\n"
  },
  {
    "path": "management/webserver/pkg/constants/detectlog.go",
    "content": "package constants\n\nconst (\n\tProtocolHTTP  = 0\n\tProtocolHTTPS = 1\n\tProtocolHTTP2 = 2\n)\n\nvar (\n\tHTTPProtocol = map[int]string{\n\t\tProtocolHTTP:  \"http\",\n\t\tProtocolHTTPS: \"https\",\n\t\t// 在 WAF 中开启 HTTP2 的前提是开启 TLS，所以这里先把 HTTP2 都兼容显示为 HTTPS\n\t\tProtocolHTTP2: \"https\",\n\t}\n\n\tAttackType = map[int]string{\n\t\t-4: \"超长数据\",\n\t\t-3: \"黑名单\",\n\t\t-2: \"白名单\",\n\t\t-1: \"非攻击\",\n\t\t0:  \"SQL 注入\",\n\t\t1:  \"XSS\",\n\t\t2:  \"CSRF\",\n\t\t3:  \"SSRF\",\n\t\t4:  \"拒绝服务\",\n\t\t5:  \"后门\",\n\t\t6:  \"反序列化\",\n\t\t7:  \"代码执行\",\n\t\t8:  \"代码注入\",\n\t\t9:  \"命令注入\",\n\t\t10: \"文件上传\",\n\t\t11: \"文件包含\",\n\t\t12: \"重定向\",\n\t\t13: \"权限不当\",\n\t\t14: \"信息泄露\",\n\t\t15: \"未授权访问\",\n\t\t16: \"不安全的配置\",\n\t\t17: \"XXE\",\n\t\t18: \"XPath 注入\",\n\t\t19: \"LDAP 注入\",\n\t\t20: \"目录穿越\",\n\t\t21: \"扫描器\",\n\t\t22: \"水平权限绕过\",\n\t\t23: \"垂直权限绕过\",\n\t\t24: \"文件修改\",\n\t\t25: \"文件读取\",\n\t\t26: \"文件删除\",\n\t\t27: \"逻辑错误\",\n\t\t28: \"CRLF 注入\",\n\t\t29: \"模板注入\",\n\t\t30: \"点击劫持\",\n\t\t31: \"缓冲区溢出\",\n\t\t32: \"整数溢出\",\n\t\t33: \"格式化字符串\",\n\t\t34: \"条件竞争\",\n\t\t35: \"HTTP 协议违规\",\n\t\t61: \"超时\",\n\t\t62: \"未知\",\n\t\t63: \"威胁情报\",\n\t\t64: \"Cookie 篡改\",\n\t}\n\n\tCountryCode = map[string]string{\n\t\t\"CN\": \"中国\",\n\t\t\"AD\": \"安道尔\",\n\t\t\"AE\": \"阿联酋\",\n\t\t\"AF\": \"阿富汗\",\n\t\t\"AG\": \"安提瓜和巴布达\",\n\t\t\"AI\": \"安圭拉\",\n\t\t\"AL\": \"阿尔巴尼亚\",\n\t\t\"AM\": \"亚美尼亚\",\n\t\t\"AO\": \"安哥拉\",\n\t\t\"AQ\": \"南极洲\",\n\t\t\"AR\": \"阿根廷\",\n\t\t\"AS\": \"美属萨摩亚\",\n\t\t\"AT\": \"奥地利\",\n\t\t\"AU\": \"澳大利亚\",\n\t\t\"AW\": \"阿鲁巴\",\n\t\t\"AX\": \"奥兰\",\n\t\t\"AZ\": \"阿塞拜疆\",\n\t\t\"BA\": \"波斯尼亚和黑塞哥维那\",\n\t\t\"BB\": \"巴巴多斯\",\n\t\t\"BD\": \"孟加拉国\",\n\t\t\"BE\": \"比利时\",\n\t\t\"BF\": \"布基纳法索\",\n\t\t\"BG\": \"保加利亚\",\n\t\t\"BH\": \"巴林\",\n\t\t\"BI\": \"布隆迪\",\n\t\t\"BJ\": \"贝宁\",\n\t\t\"BL\": \"圣巴泰勒米\",\n\t\t\"BM\": \"百慕大\",\n\t\t\"BN\": \"文莱\",\n\t\t\"BO\": \"玻利维亚\",\n\t\t\"BQ\": \"加勒比荷兰\",\n\t\t\"BR\": \"巴西\",\n\t\t\"BS\": \"巴哈马\",\n\t\t\"BT\": \"不丹\",\n\t\t\"BV\": \"布韦岛\",\n\t\t\"BW\": \"博茨瓦纳\",\n\t\t\"BY\": \"白俄罗斯\",\n\t\t\"BZ\": \"伯利兹\",\n\t\t\"CA\": \"加拿大\",\n\t\t\"CC\": \"科科斯（基林）群岛\",\n\t\t\"CD\": \"刚果（金）\",\n\t\t\"CF\": \"中非\",\n\t\t\"CG\": \"刚果（布）\",\n\t\t\"CH\": \"瑞士\",\n\t\t\"CI\": \"科特迪瓦\",\n\t\t\"CK\": \"库克群岛\",\n\t\t\"CL\": \"智利\",\n\t\t\"CM\": \"喀麦隆\",\n\t\t\"CO\": \"哥伦比亚\",\n\t\t\"CR\": \"哥斯达黎加\",\n\t\t\"CU\": \"古巴\",\n\t\t\"CV\": \"佛得角\",\n\t\t\"CW\": \"库拉索\",\n\t\t\"CX\": \"圣诞岛\",\n\t\t\"CY\": \"塞浦路斯\",\n\t\t\"CZ\": \"捷克\",\n\t\t\"DE\": \"德国\",\n\t\t\"DJ\": \"吉布提\",\n\t\t\"DK\": \"丹麦\",\n\t\t\"DM\": \"多米尼克\",\n\t\t\"DO\": \"多米尼加\",\n\t\t\"DZ\": \"阿尔及利亚\",\n\t\t\"EC\": \"厄瓜多尔\",\n\t\t\"EE\": \"爱沙尼亚\",\n\t\t\"EG\": \"埃及\",\n\t\t\"EH\": \"阿拉伯撒哈拉民主共和国\",\n\t\t\"ER\": \"厄立特里亚\",\n\t\t\"ES\": \"西班牙\",\n\t\t\"ET\": \"埃塞俄比亚\",\n\t\t\"FI\": \"芬兰\",\n\t\t\"FJ\": \"斐济\",\n\t\t\"FK\": \"福克兰群岛\",\n\t\t\"FM\": \"密克罗尼西亚联邦\",\n\t\t\"FO\": \"法罗群岛\",\n\t\t\"FR\": \"法国\",\n\t\t\"GA\": \"加蓬\",\n\t\t\"GB\": \"英国\",\n\t\t\"GD\": \"格林纳达\",\n\t\t\"GE\": \"格鲁吉亚\",\n\t\t\"GF\": \"法属圭亚那\",\n\t\t\"GG\": \"根西\",\n\t\t\"GH\": \"加纳\",\n\t\t\"GI\": \"直布罗陀\",\n\t\t\"GL\": \"格陵兰\",\n\t\t\"GM\": \"冈比亚\",\n\t\t\"GN\": \"几内亚\",\n\t\t\"GP\": \"瓜德罗普\",\n\t\t\"GQ\": \"赤道几内亚\",\n\t\t\"GR\": \"希腊\",\n\t\t\"GS\": \"南乔治亚和南桑威奇群岛\",\n\t\t\"GT\": \"危地马拉\",\n\t\t\"GU\": \"关岛\",\n\t\t\"GW\": \"几内亚比绍\",\n\t\t\"GY\": \"圭亚那\",\n\t\t\"HM\": \"赫德岛和麦克唐纳群岛\",\n\t\t\"HN\": \"洪都拉斯\",\n\t\t\"HR\": \"克罗地亚\",\n\t\t\"HT\": \"海地\",\n\t\t\"HU\": \"匈牙利\",\n\t\t\"ID\": \"印尼\",\n\t\t\"IE\": \"爱尔兰\",\n\t\t\"IL\": \"以色列\",\n\t\t\"IM\": \"马恩岛\",\n\t\t\"IN\": \"印度\",\n\t\t\"IO\": \"英属印度洋领地\",\n\t\t\"IQ\": \"伊拉克\",\n\t\t\"IR\": \"伊朗\",\n\t\t\"IS\": \"冰岛\",\n\t\t\"IT\": \"意大利\",\n\t\t\"JE\": \"泽西\",\n\t\t\"JM\": \"牙买加\",\n\t\t\"JO\": \"约旦\",\n\t\t\"JP\": \"日本\",\n\t\t\"KE\": \"肯尼亚\",\n\t\t\"KG\": \"吉尔吉斯斯坦\",\n\t\t\"KH\": \"柬埔寨\",\n\t\t\"KI\": \"基里巴斯\",\n\t\t\"KM\": \"科摩罗\",\n\t\t\"KN\": \"圣基茨和尼维斯\",\n\t\t\"KP\": \"朝鲜\",\n\t\t\"KR\": \"韩国\",\n\t\t\"KW\": \"科威特\",\n\t\t\"KY\": \"开曼群岛\",\n\t\t\"KZ\": \"哈萨克斯坦\",\n\t\t\"LA\": \"老挝\",\n\t\t\"LB\": \"黎巴嫩\",\n\t\t\"LC\": \"圣卢西亚\",\n\t\t\"LI\": \"列支敦士登\",\n\t\t\"LK\": \"斯里兰卡\",\n\t\t\"LR\": \"利比里亚\",\n\t\t\"LS\": \"莱索托\",\n\t\t\"LT\": \"立陶宛\",\n\t\t\"LU\": \"卢森堡\",\n\t\t\"LV\": \"拉脱维亚\",\n\t\t\"LY\": \"利比亚\",\n\t\t\"MA\": \"摩洛哥\",\n\t\t\"MC\": \"摩纳哥\",\n\t\t\"MD\": \"摩尔多瓦\",\n\t\t\"ME\": \"黑山\",\n\t\t\"MF\": \"法属圣马丁\",\n\t\t\"MG\": \"马达加斯加\",\n\t\t\"MH\": \"马绍尔群岛\",\n\t\t\"MK\": \"马其顿\",\n\t\t\"ML\": \"马里\",\n\t\t\"MM\": \"缅甸\",\n\t\t\"MN\": \"蒙古\",\n\t\t\"MP\": \"北马里亚纳群岛\",\n\t\t\"MQ\": \"马提尼克\",\n\t\t\"MR\": \"毛里塔尼亚\",\n\t\t\"MS\": \"蒙特塞拉特\",\n\t\t\"MT\": \"马耳他\",\n\t\t\"MU\": \"毛里求斯\",\n\t\t\"MV\": \"马尔代夫\",\n\t\t\"MW\": \"马拉维\",\n\t\t\"MX\": \"墨西哥\",\n\t\t\"MY\": \"马来西亚\",\n\t\t\"MZ\": \"莫桑比克\",\n\t\t\"NA\": \"纳米比亚\",\n\t\t\"NC\": \"新喀里多尼亚\",\n\t\t\"NE\": \"尼日尔\",\n\t\t\"NF\": \"诺福克岛\",\n\t\t\"NG\": \"尼日利亚\",\n\t\t\"NI\": \"尼加拉瓜\",\n\t\t\"NL\": \"荷兰\",\n\t\t\"NO\": \"挪威\",\n\t\t\"NP\": \"尼泊尔\",\n\t\t\"NR\": \"瑙鲁\",\n\t\t\"NU\": \"纽埃\",\n\t\t\"NZ\": \"新西兰\",\n\t\t\"OM\": \"阿曼\",\n\t\t\"PA\": \"巴拿马\",\n\t\t\"PE\": \"秘鲁\",\n\t\t\"PF\": \"法属波利尼西亚\",\n\t\t\"PG\": \"巴布亚新几内亚\",\n\t\t\"PH\": \"菲律宾\",\n\t\t\"PK\": \"巴基斯坦\",\n\t\t\"PL\": \"波兰\",\n\t\t\"PM\": \"圣皮埃尔和密克隆\",\n\t\t\"PN\": \"皮特凯恩群岛\",\n\t\t\"PR\": \"波多黎各\",\n\t\t\"PS\": \"巴勒斯坦\",\n\t\t\"PT\": \"葡萄牙\",\n\t\t\"PW\": \"帕劳\",\n\t\t\"PY\": \"巴拉圭\",\n\t\t\"QA\": \"卡塔尔\",\n\t\t\"RE\": \"留尼汪\",\n\t\t\"RO\": \"罗马尼亚\",\n\t\t\"RS\": \"塞尔维亚\",\n\t\t\"RU\": \"俄罗斯\",\n\t\t\"RW\": \"卢旺达\",\n\t\t\"SA\": \"沙特阿拉伯\",\n\t\t\"SB\": \"所罗门群岛\",\n\t\t\"SC\": \"塞舌尔\",\n\t\t\"SD\": \"苏丹\",\n\t\t\"SE\": \"瑞典\",\n\t\t\"SG\": \"新加坡\",\n\t\t\"SH\": \"圣赫勒拿\",\n\t\t\"SI\": \"斯洛文尼亚\",\n\t\t\"SJ\": \"挪威\",\n\t\t\"SK\": \"斯洛伐克\",\n\t\t\"SL\": \"塞拉利昂\",\n\t\t\"SM\": \"圣马力诺\",\n\t\t\"SN\": \"塞内加尔\",\n\t\t\"SO\": \"索马里\",\n\t\t\"SR\": \"苏里南\",\n\t\t\"SS\": \"南苏丹\",\n\t\t\"ST\": \"圣多美和普林西比\",\n\t\t\"SV\": \"萨尔瓦多\",\n\t\t\"SX\": \"荷属圣马丁\",\n\t\t\"SY\": \"叙利亚\",\n\t\t\"SZ\": \"斯威士兰\",\n\t\t\"TC\": \"特克斯和凯科斯群岛\",\n\t\t\"TD\": \"乍得\",\n\t\t\"TF\": \"法属南方和南极洲领地\",\n\t\t\"TG\": \"多哥\",\n\t\t\"TH\": \"泰国\",\n\t\t\"TJ\": \"塔吉克斯坦\",\n\t\t\"TK\": \"托克劳\",\n\t\t\"TL\": \"东帝汶\",\n\t\t\"TM\": \"土库曼斯坦\",\n\t\t\"TN\": \"突尼斯\",\n\t\t\"TO\": \"汤加\",\n\t\t\"TR\": \"土耳其\",\n\t\t\"TT\": \"特立尼达和多巴哥\",\n\t\t\"TV\": \"图瓦卢\",\n\t\t\"TZ\": \"坦桑尼亚\",\n\t\t\"UA\": \"乌克兰\",\n\t\t\"UG\": \"乌干达\",\n\t\t\"UM\": \"美国本土外小岛屿\",\n\t\t\"US\": \"美国\",\n\t\t\"UY\": \"乌拉圭\",\n\t\t\"UZ\": \"乌兹别克斯坦\",\n\t\t\"VA\": \"梵蒂冈\",\n\t\t\"VC\": \"圣文森特和格林纳丁斯\",\n\t\t\"VE\": \"委内瑞拉\",\n\t\t\"VG\": \"英属维尔京群岛\",\n\t\t\"VI\": \"美属维尔京群岛\",\n\t\t\"VN\": \"越南\",\n\t\t\"VU\": \"瓦努阿图\",\n\t\t\"WF\": \"瓦利斯和富图纳\",\n\t\t\"WS\": \"萨摩亚\",\n\t\t\"YE\": \"也门\",\n\t\t\"YT\": \"马约特\",\n\t\t\"ZA\": \"南非\",\n\t\t\"ZM\": \"赞比亚\",\n\t\t\"ZW\": \"津巴布韦\",\n\t}\n\n\tRuleModule = map[string]string{\n\t\t\"m_sqli\":               \"SQL 注入检测模块\",\n\t\t\"m_xss\":                \"XSS 检测模块\",\n\t\t\"m_csrf\":               \"CSRF 检测模块\",\n\t\t\"m_ssrf\":               \"SSRF 检测模块\",\n\t\t\"m_php_unserialize\":    \"PHP 反序列化检测模块\",\n\t\t\"m_java_unserialize\":   \"Java 反序列化检测模块\",\n\t\t\"m_file_upload\":        \"文件上传攻击检测模块\",\n\t\t\"m_file_include\":       \"文件包含攻击检测模块\",\n\t\t\"m_php_code_injection\": \"PHP 代码注入检测模块\",\n\t\t\"m_java\":               \"Java 代码注入检测模块\",\n\t\t\"m_cmd_injection\":      \"命令注入检测模块\",\n\t\t\"m_response\":           \"服务器响应检测模块\",\n\t\t\"m_scanner\":            \"机器人检测模块\",\n\t\t\"m_http\":               \"畸形 HTTP 协议检测模块\",\n\t\t\"m_asp_code_injection\": \"ASP 代码注入检测模块\",\n\t\t\"m_ssti\":               \"模板注入检测模块\",\n\t\t\"m_rule\":               \"通用漏洞模块\",\n\t\t\"m_timeout\":            \"\", // 检测超时\n\t\t\"whitelist\":            \"白名单\",\n\t\t\"blacklist\":            \"黑名单\",\n\t}\n\n\t// RuleReason TODO: get from libfusion.so, refer to skyview `Fusion.get_rule_detail_dict()`\n\tRuleReason = map[string]string{\n\t\t\"6f4922f45568161a8cdf4ad2299f6d23\": \"访问测试文件的请求\",\n\t\t\"c6b99e08e56911ecb18f00155d694977\": \"[HW2021] 深信服终端检测平台远程命令执行漏洞(CNVD-2020-46552)\",\n\t\t\"c9a2f2f5b1035a1ca3a5aa776914e393\": \"[HW2020] GitLab 文件读写 (CVE-2017-0915, CVE-2016-9086)\",\n\t\t\"97e264c2235c4ad0aa61ecd68fa53351\": \"jenkins 管理员权限开放 (CVE-2018-1999001)\",\n\t\t\"ee41775801ab11ed90a200155ddb8f4e\": \"Apache HTTPD SSRF (CVE-2021-40438)\",\n\t\t\"33e75ff09dd601bbe69f351039152189\": \"访问系统文件的请求\",\n\t\t\"1c383cd30b7c298ab50293adfecb7b18\": \"Struts2 Java 代码注入漏洞\",\n\t\t\"eff4046e0b0411ed976f00155dd454dd\": \"蓝凌OA漏洞\",\n\t\t\"a1d0c6e83f027327d8461063f4ac58a6\": \"webshell\",\n\t\t\"d92ebbf7683c5cafbc74456c73bc5d0c\": \"[HW2020] JIRA OAuth SSRF (CVE-2017-9506)\",\n\t\t\"dbc22aa2fce011eca0e800163e345065\": \"[HW2020] Nginx range filter overflow (CVE-2017-7529)\",\n\t\t\"ae971210f5f411ecae9200163e345065\": \"[HW2022] VMware 认证绕过漏洞 (CVE-2022-22972)\",\n\t\t\"5af4619c46045568b4d701ad5208eee0\": \"[HW2020] Druid 未授权访问\",\n\t\t\"fbd7939d674997cdb4692d34de8633c4\": \"Jackson 反序列化 (CVE-2017-17485)\",\n\t\t\"63400573dc155253869bc90e341708e9\": \"[HW2020] Apache Spark 未授权访问\",\n\t\t\"37274a94eadf11eca1a600155dcfc445\": \"[HW2022] Apache Solr SSRF漏洞 (CVE-2021-27905)\",\n\t\t\"c4ca4238a0b923820dcc509a6f75849b\": \"PHP 代码泄露\",\n\t\t\"3416a75f4cea9109507cacd8e2f2aefc\": \"phpinfo 泄露\",\n\t\t\"c9f0f895fb98ab9159f51fd0297e236d\": \"访问敏感文件的请求\",\n\t\t\"36564acfab005dcda413a0b489ce8a02\": \"[HW2020] ffmpeg SSRF (CVE-2016-1898)\",\n\t\t\"31a7ad75521152eca43e7eb15065e850\": \"[HW2020] Kibana远程代码执行(CVE-2019-7609)\",\n\t\t\"03d0598da679b4d68089730de888a945\": \"Laravel Debug 远程代码执行 (CVE-2021-3129)\",\n\t\t\"34173cb38f07f89ddbebc2ac9128303f\": \"XML 实体注入漏洞\",\n\t\t\"09afe350746411eda37500163e12b978\": \"Apache Kylin 未授权配置 (CVE-2020-13927)\",\n\t\t\"4096d530f60c11eca0ad00163e345065\": \"[HW2020] Horde Groupware Webmail Edition 远程命令执行漏洞\",\n\t\t\"c6b57d8ce56911ec980b00155d694977\": \"[HW2021] F5 BIG-IP 远程代码执行漏洞(CVE-2020-5902)\",\n\t\t\"8964339e01b411ed9bdc00163e345065\": \"Spring Cloud Config 路径穿越漏洞 (CVE-2020-5405)\",\n\t\t\"e336d501c94e556d8fa3f6df584e88e2\": \"[HW2020] Spring WebFlow RCE (CVE-2017-4971)\",\n\t\t\"c6b9fa2ee56911ecb64f00155d694977\": \"[HW2021] 通达OA权限提升漏洞(11.5.200417 之前的版本)\",\n\t\t\"8f1c07fe747c11ed8b7c00163e12b978\": \"Microsoft Exchange 远程代码执行 (CVE-2020-0688)\",\n\t\t\"3ef815416f775098fe977004015c6193\": \"Apache Solr 远程代码执行漏洞 (CVE-2019-0193)\",\n\t\t\"2e7f58dd541d5c478fc12bc8a64bef41\": \"[HW2020] S2-005 (2) (CVE-2010-1870)\",\n\t\t\"1ff1de774005f8da13f42943881c655f\": \"DEDECMS add suffix\",\n\t\t\"458c1b2af1f911ecafdb00163e345065\": \"[HW2021] Sitecore XP远程代码执行漏洞(CVE-2021-42237)\",\n\t\t\"ac3d6ee2714b11ed87ba00163e12b978\": \"通达OA V11.x远程代码执行漏洞\",\n\t\t\"9fa20cc8eabf11ec808600155dcfc445\": \"[HW2022] 360天擎信息泄露\",\n\t\t\"2a1f6e4292ee30c1a92d3e8333471534\": \"Jackson 反序列化\",\n\t\t\"e404c364f1f511ec9cc500163e345065\": \"[HW2021] Zabbix 5.0.17-Remote Code Execution (RCE) (Authenticated)\",\n\t\t\"c6b8cd66e56911ec8cc100155d694977\": \"[HW2021] 宝塔面板数据库管理未授权访问漏洞(Linux正式版7.4.2、Linux测试版7.5.13、Windows正式版6.8)\",\n\t\t\"3c27c530751511edbaf200163e12b978\": \"Citrix 未授权访问 \",\n\t\t\"22aaa784da604abeb19b0aeee2b3cc6d\": \"Jenkins 非预期方法调用漏洞(SECURITY-595)\",\n\t\t\"a5bfc9e07964f8dddeb95fc584cd965d\": \"Java 畸形 double 数据 DOS 漏洞\",\n\t\t\"c6babe8ce56911eca76200155d694977\": \"[HW2021] 致远OA 文件上传漏洞(V8.0、V7.1、V7.1SP1、V7.0、V7.0SP1、V7.0SP2、V7.0SP3、V6.0、V6.1SP1、V6.1SP2、V5.x)\",\n\t\t\"d7e82c628f96541c924637f711555266\": \"[HW2020] PHPStudy 命令执行\",\n\t\t\"7f39f8317fbdb1988ef4c628eba02591\": \"访问 IIS 短文件名 / 文件夹\",\n\t\t\"44f683a84163b3523afe57c2e008bc8c\": \"扫描 IIS 短文件名 / 文件夹\",\n\t\t\"c38555f8746411eda4a400163e12b978\": \"快排CMS 未授权访问 \",\n\t\t\"866cbe77d2884225a8fa19793cc8ad51\": \"ghostscript 命令执行 (CVE-2018-19475)\",\n\t\t\"17400b535f9f5db5a8424265ed8cc38c\": \"[HW2020] JeeCMS SSRF\",\n\t\t\"774b89ca853942cabc8b94bfff46d73c\": \"ECShop 2.x-3.x 远程代码执行漏洞\",\n\t\t\"b801a4a10a22422dff885812a5d0d417\": \"科蓝反序列化漏洞\",\n\t\t\"6c991e5e23215c589a86f13ae800c616\": \"[HW2020] Couchdb 垂直越权 (1) (CVE-2017-12635)\",\n\t\t\"7d5a620cf69311ecbf8400163e345065\": \"[HW2020] ShardingShpere远程命令执行漏洞 (CVE-2020-1947)\",\n\t\t\"a26afe82eade11ecb85f00155dcfc445\": \"[HW2022] Alibaba Nacos认证绕过\",\n\t\t\"3343e2def60b11ec964400163e345065\": \"[HW2021] 用友ERP-NC 任意文件读取漏洞 (2021-?-?)\",\n\t\t\"09189cf61c0e4774a4d8af7bfb0cf6de\": \"WebLogic wls9-async 和 wls-wsat 反序列化 (CNVD-C-2019-48814)\",\n\t\t\"738430de6f054031a7597c00eb1fd061\": \"Drupal Mail 命令注入\",\n\t\t\"2abf53869fd05cf5a69c11ea42d2cb54\": \"[HW2020] ActiveMQ 任意文件写入漏洞 (CVE-2016-3088)\",\n\t\t\"e2c420d928d4bf8ce0ff2ec19b371514\": \"Drupal 内核远程代码执行漏洞 (CVE-2018-7602)\",\n\t\t\"ac61a980eae411ec985100155dcfc445\": \"[HW2022] Zyxel NBG2105 身份验证绕过 (CVE-2021-3297)\",\n\t\t\"d67d8ab4f4c10bf22aa353e27879133c\": \"PHP168 代码执行漏洞\",\n\t\t\"e7629f3e750a11eda3a900163e12b978\": \"PbootCms v3.1.2 远程代码执行 \",\n\t\t\"6d85a117690c32af50f9b47f98abf408\": \"骑士 CMS 远程代码执行\",\n\t\t\"4ebd2f6aee2811ec8e2600163e345065\": \"[HW2022] 齐治堡垒机任意用户登录漏洞\",\n\t\t\"688142e95d6d446ee901de2b4d087a5d\": \"F5 BIG-IP漏洞\",\n\t\t\"91d18ff6f2dc54618b1a28f15afe572f\": \"[HW2020] Couchdb 命令执行 (CVE-2017-12636)\",\n\t\t\"70efdf2ec9b086079795c442636b55fb\": \"访问管理后台的请求\",\n\t\t\"6d6bcd8ebc2c4d4a961ce2e8b4b3af67\": \"joomla 远程代码执行\",\n\t\t\"c6b4be74e56911eca25c00155d694977\": \"[HW2021] Apache Shiro身份认证绕过漏洞(CVE-2020-17523)\",\n\t\t\"1f0822d21d0a11edad0900163e345065\": \"[HW2020] Couchdb 垂直越权 (1) (CVE-2017-12635)\",\n\t\t\"2024042cf11511ecb17e00163e345065\": \"[HW2021] GoAhead Server 环境变量注入漏洞(CVE-2021-42342)\",\n\t\t\"abade038ebc211ec8a8800163e345065\": \"[HW2022] VMware SSRF XSS)\",\n\t\t\"32ac3f49feb65bada13cdf85722d237a\": \"[HW2020] Spring RCE (CVE-2018-1270)\",\n\t\t\"43ec517d68b6edd3015b3edc9a11367b\": \"Spring 框架漏洞\",\n\t\t\"6a48a52c11424cd68e89d26d212556c4\": \"ThinkPHP5 任意代码执行漏洞\",\n\t\t\"a5d9fe3b0172b4625a1fe6a41482aa8b\": \"Xstream 反序列化\",\n\t\t\"072b030ba126b2f4b2374f342be9ed44\": \"路径穿越攻击\",\n\t\t\"38529f8eebc211eca87c00163e345065\": \"[HW2022] VMware 任意文件读取\",\n\t\t\"3a3128d0e7e811ec909000155db4c628\": \"Java 代码注入\",\n\t\t\"e369853df766fa44e1ed0ff613f563bd\": \"Struts2 S2-020\",\n\t\t\"d3d9446802a44259755d38e6d163e820\": \"访问 Git 仓库的请求\",\n\t\t\"6e4b3bcf45295dcc9b1f3efef4353287\": \"[HW2020] JIRA SSRF (CVE-2019-8451)\",\n\t\t\"b58a180cf11111ec8a4600163e345065\": \"[HW2022] VMware 服务端模板注入漏洞(CVE-2022-22954)\",\n\t\t\"7cbbc409ec990f19c78c75bd1e06f215\": \"Jenkins 远程代码执行漏洞 (CVE-2018-1000861)\",\n\t\t\"c6b6f9d2e56911ec8e2e00155d694977\": \"[HW2021] phpStudy nginx 解析漏洞(phptsuy8.1.07的Nginx1.5.11版本)\",\n\t\t\"b4b143a12d0d5eeabef67b6597dc7cd9\": \"[HW2020] DedeCMS 密码重置漏洞\",\n\t\t\"028c274cfc4f11ecaedd00163e345065\": \"[HW2022] Oracle Access Manager RCE (CVE-2021-35587)\",\n\t\t\"1fc707c92da6519e9337f202ad1fd959\": \"[HW2020] 通达OA 前台任意用户登录\",\n\t\t\"2efd7872752f11ed97f600163e12b978\": \"XStream 反序列化代码执行 (CVE-2021-29505)\",\n\t\t\"9355da5d436e52cba6fd2012ad5a5832\": \"[HW2020] Spring Data Rest RCE (CVE-2017-8046)\",\n\t\t\"d09bf41544a3365a46c9077ebb5e35c3\": \"Jackson 反序列化 (CVE-2017-7525)\",\n\t\t\"c20ad4d76fe97759aa27a0c99bff6710\": \"访问 SVN 仓库的请求\",\n\t\t\"e1c17c16169b58078be071b37eda9f3c\": \"[HW2020] HFS RCE\",\n\t\t\"e4a2355fa844484bbdf799cc8795b415\": \"Apache Druid 远程命令执行漏洞\",\n\t\t\"72c04a1fbd6e5641bb33d0185dc36b9b\": \"[HW2020] Nexus 后台表达式注入 (CVE-2020-10199, CVE-2019-7238)\",\n\t\t\"c6b519e6e56911ecaa1c00155d694977\": \"[HW2021] Drupal任意PHP代码执行漏洞(CVE-2020-28948/28949)\",\n\t\t\"c81e728d9d4c2f636f067f89cc14862c\": \"访问敏感文件的请求\",\n\t\t\"a02582663945fa213788203624eb3b89\": \"Jackson-databind 反序列化 (CVE-2020-35790/CVE-2020-35491)\",\n\t\t\"45c48cce2e2d7fbdea1afc51c7c6ad26\": \"访问 Git 仓库的请求\",\n\t\t\"029e67ff376659daaea612ec5c3964ab\": \"[HW2020] Gitea LFS RCE\",\n\t\t\"e52769085be711ec859e00155dae391c\": \"Apache Log4j 远程命令执行漏洞\",\n\t\t\"19ca14e7ea6328a42e0eb13d585e4c22\": \"Java 代码注入漏洞 (针对 Struts 2 漏洞的通用防御)\",\n\t\t\"3c59dc048e8850243be8079a5c74d079\": \"CVE-2012-1823 PHP FastCGI 远程代码执行漏洞\",\n\t\t\"14766b63abf447ac808b92535dde9c09\": \"Gogs、Gitea 远程代码执行漏洞 (CVE-2018-18925)\",\n\t\t\"9bf31c7ff062936a96d3c8bd1f8f2ff3\": \"后门\",\n\t\t\"ae0196baf5f211ec98c400163e345065\": \"[HW2022] Sunlogin-11.0.0.33-RCE-1_获取token\",\n\t\t\"2838023a778dfaecdc212708f721b788\": \"访问备份或临时文件的请求\",\n\t\t\"110ad086eaff11ec87c500155d143f7b\": \"[HW2022] Gitlab exiftool 远程命令执行漏洞 (CVE-2021-22205)\",\n\t\t\"e3959e0e30494d6caa1e9b259073d96c\": \"南方数据管理员添加未授权访问漏洞\",\n\t\t\"98f13708210194c475687be6106a3b84\": \"Nginx code 解析漏洞\",\n\t\t\"887d1820a85a43cfbeee578a02f6a914\": \"fastjson 反序列化代码执行\",\n\t\t\"88a4a7227e81d064b551f52f7f51e6c8\": \"泛微 E-cology OA远程代码执行\",\n\t\t\"4c15abcbe6e8410aac559817925bfd11\": \"TBK DVR 验证绕过漏洞 (CVE-2018-9995)\",\n\t\t\"460072c82cca58999281247426c71bf8\": \"[HW2020] ffmpeg 文件读取 (CVE-2016-1897)\",\n\t\t\"96c4bc6a6adf4434b93d714106f4c67f\": \"ghostscript 任意文件读写 (CVE-2018-17961)\",\n\t\t\"9dbe327d8dd64f3a8d919cf19d5e3e00\": \"ThinkPHP 5.0.x _method 代码注入\",\n\t\t\"6512bd43d9caa6e02c990b0a82652dca\": \"访问 SVN 仓库的请求\",\n\t\t\"a12856ca51bb5ed69b6646dafd605e7c\": \"[HW2020] Apache Shiro 反序列化攻击 (CVE-2016-4437)\",\n\t\t\"9473dea2eb144fee9ddeac33b49358b9\": \"Nexus Repository Manager 3 远程代码执行漏洞(CVE-2019-7238)\",\n\t\t\"eccbc87e4b5ce2fe28308fd9f2a7baf3\": \"Apache 目录遍历\",\n\t\t\"e799871cf6b653db94b759b79deefe5d\": \"[HW2020] ImageMagick RCE (CVE-2016-3714)\",\n\t\t\"ca037bccfa7c5e7b822ea1889ba7be47\": \"[HW2020] MetInfo 前台 SQL 注入 (CNVD-2018-20024)\",\n\t\t\"638d9bb801b611eda81e00163e345065\": \"Atlassian Jira 服务端请求伪造漏洞 (CVE-2022-26135)\",\n\t\t\"8ef24d7501e85d1fa3ce4016dbf297d1\": \"[HW2020] Webmin RCE (CVE-2019-15107)\",\n\t\t\"81f360f5033c55179d34b37b61389503\": \"[HW2020] Palo Alto GlobalProtect SSL RCE (CVE-2019-1579)\",\n\t\t\"c374fb01134a4213be9ba67539523162\": \"IIS 扩展名漏洞 (CVE-2009-4444)\",\n\t\t\"21203602eafd11ec8dc300155d143f7b\": \"[HW2022] F5 BIG-IP 远程代码执行漏洞 (CVE-2021-22986)\",\n\t\t\"2cba1bd57c9603c07ff4f09f8e4fb0f9\": \"Apache AXIS 远程命令执行漏洞\",\n\t\t\"a0ff28f2f5f111eca2de00163e345065\": \"[HW2022] Weblogic Server 信息泄漏漏洞利用\",\n\t\t\"d645920e395fedad7bbbed0eca3fe2e0\": \"DEDECMS SQL 注入漏洞\",\n\t\t\"2b6e15ac747811eda03000163e12b978\": \"Apache DolphinScheduler 远程代码执行 (CVE-2020-11974)\",\n\t\t\"1f5449318bbc38d200fb9f3bcf87a50f\": \"WebLogic console 远程代码执行漏洞 (CVE-2020-14882)\",\n\t\t\"02e74f10e0327ad868d138f2b4fdd6f0\": \"利用 Apache 解析漏洞来执行 PHP 脚本\",\n\t\t\"3ff4aed51a7c506087c0b9df1b6ac0d3\": \"[HW2020] Couchdb 垂直越权 (2) (CVE-2017-12635)\",\n\t\t\"933cbfbbc71c417ab9d757e5dc4be4d4\": \"Jenkins Accept-Language 信息泄露 (CVE-2018-1999002)\",\n\t\t\"182be0c5cdcd5072bb1864cdee4d3d6e\": \"Struts2 S2-016\",\n\t\t\"6af6b17ceae711ecbd5100155dcfc445\": \"[HW2022] 三星 WLAN AP WEA453e路由器 RCE (query)\",\n\t\t\"7f41d19e081411edaca100163e345065\": \"[HW2022] 三星 WLAN AP WEA453e路由器 RCE (form)\",\n\t\t\"cfcd208495d565ef66e7dff9f98764da\": \"JSP 代码泄露\",\n\t\t\"3b57f3413df14b2380ae8e483cb0dcd5\": \"Drupalgeddon2 - Drupal核心远程代码执行\",\n\t\t\"6364d3f0f495b6ab9dcf8d3b5c6e0b01\": \"Struts2 S2-005\",\n\t\t\"a98e8c0cf12211ecb1d800163e345065\": \"[HW2021] VMware vCenter Server 远程代码执行漏洞(CVE-2021-21985)\",\n\t\t\"b42530a1ea025fa78bb0c6c7d2e1ed95\": \"[HW2020] PHP FPM RCE (CVE-2019-11043)\",\n\t\t\"bbe199e1efec469588fde16dce75df36\": \"coremail 信息泄露\",\n\t\t\"c6b7547ce56911ec9d3d00155d694977\": \"[HW2021] SaltStack多个高危漏洞(CVE-2020-16846,CVE-2020-17490,CVE-2020-25592)\",\n\t\t\"c6b7ae5ee56911ec9d1600155d694977\": \"[HW2021] SAP NetWeaver AS JAVA 高危漏洞(CVE-2020-6287)\",\n\t\t\"65d86160f1f711ec971800163e345065\": \"[HW2021] Total.js框架远程代码执行漏洞(CVE-2021-23389)\",\n\t\t\"aa4755eb0fdf47ec91e8f285613e7954\": \"Atlassian 漏洞\",\n\t\t\"c6b5dc64e56911ecbe7300155d694977\": \"[HW2021] Microsoft Exchange远程代码执行漏洞(CVE-2020-16875)\",\n\t\t\"b6f76f653ded54b494292eb6c285d464\": \"[HW2020] WebLogic 弱口令\",\n\t\t\"9778d5d219c5080b9a6a17bef029331c\": \"Apache Solr 远程代码执行漏洞 (CVE-2019-0192)\",\n\t\t\"c6bb1f3ae56911ec93a500155d694977\": \"[HW2021] WebSphere 权限提升漏洞(CVE-2020-4276)\",\n\t\t\"c6ba5e10e56911ec8f6d00155d694977\": \"[HW2021] 微软 SQL Server 报表服务远程代码执行漏洞(CVE-2020-0618)\",\n\t\t\"061de838eabf11ec8ee500155dcfc445\": \"[HW2022] 360天擎SQL注入漏洞\",\n\t\t\"6ea9ab1baa0efb9e19094440c317e21b\": \"XML 实体注入漏洞\",\n\t\t\"039c966635bd4ac286553f55d9457615\": \"dedecms 5.7 升级时间泄露\",\n\t\t\"b6d767d2f8ed5d21a44b0e5886680cb9\": \"DEDECMS add suffix\",\n\t\t\"f033ab37c30201f73f142449d037028d\": \"ActiveMQ 任意文件写入漏洞 (CVE-2016-3088)\",\n\t\t\"558fce096ca65beb8f7ca7d4b2bf6b4f\": \"[HW2020] ElasticSearch 命令执行 (CVE-2015-1427)\",\n\t\t\"b53b3a3d6ab90ce0268229151c9bde11\": \"代码注入漏洞 (针对 WordPress)\",\n\t\t\"c6b4239ce56911ec8b8a00155d694977\": \"[HW2021] Apache Shiro 权限绕过漏洞(CVE-2020-13933)\",\n\t\t\"2408e35abe40580286b6d9106b43357f\": \"[HW2020] S2-005 (1) (CVE-2010-1870)\",\n\t\t\"724390fd0bf29ab3adb92ee026474fe1\": \"Jackson-databind 反序列化 (CVE-2020-36179~CVE-2020-36189)\",\n\t\t\"ef268f2b31fd5943bd1882e2218e9924\": \"[HW2020] Tomcat RCE (CVE-2019-0232)\",\n\t\t\"d49bb7ebe41146deb48138f28d220c7f\": \"可疑远程调用协议\",\n\t\t\"aab3238922bcc25a6f606eb525ffdc56\": \"phpinfo 泄露\",\n\t\t\"582e036cee2811ec90c700163e345065\": \"[HW2022] H3C SecPath运维审计系统任意用户登录漏洞\",\n\t\t\"25d513abf19740b083f2262270c0a281\": \"PHPCMS v9.6.0 任意文件上传\",\n\t\t\"ab299838fbe1584d843fc004b0034e81\": \"[HW2020] GoAhead RCE (CVE-2017-17562)\",\n\t\t\"c6b81b78e56911ec989800155d694977\": \"[HW2021] SolarWinds 远程代码执行漏洞(CVE-2020-10148)\",\n\t\t\"daf7d4a0116711ed8d9c00163e35b03e\": \"Seacms php代码注入\",\n\t\t\"c6b93e5ee56911ec8e1500155d694977\": \"[HW2021] 深信服终端检测平台任意用户登录(CNVD-2020-46552)\",\n\t\t\"95a4035353ed405ba3418698fc06b544\": \"spring 目录遍历 (cve-2014-3625)\",\n\t\t\"eab53f221ee611edb57e00163e345065\": \"VMware Workspace ONE UEM SSRF (CVE-2021-22054)\",\n\t\t\"fca34064ebc211ec9b9900163e345065\": \"[HW2022] VMware vRealize SSRF (CVE-2021-21975)\",\n\t\t\"c51ce410c124a10e0db5e4b97fc2af39\": \"PHPSpy 后门\",\n\t\t\"2f7e97b9204b5239b089c5ab83242519\": \"[HW2020] ElasticSearch 未授权访问\",\n\t\t\"6e34a9cb6f5b43e4973b170e626a2f77\": \"Apache APISIX lua 远程代码执行\",\n\t\t\"299ce1c43af85c7987d8ddef915888a8\": \"[HW2020] Zimbra RCE\",\n\t\t\"529d97a8f5d711eca1e700163e345065\": \"[HW2022] Weblogic Server 信息泄漏漏洞利用 (CVE-2022-21371)\",\n\t\t\"49234868ee1f11ec833b00155de98ae1\": \"[HW2022] AVTECH监控后台命令注入漏洞\",\n\t\t\"1f0e3dad99908345f7439f8ffabdffc4\": \"PHP FastCGI 解析漏洞\",\n\t}\n)\n"
  },
  {
    "path": "management/webserver/pkg/constants/detector.go",
    "content": "package constants\n\nconst (\n\tContentType          = \"application/octet-stream\"\n\tUpdateEntrypoint     = \"/update/policy\"\n\tStatEntrypoint       = \"/stat\"\n\tDefaultPolicyVersion = \"1\"\n)\n"
  },
  {
    "path": "management/webserver/pkg/constants/option.go",
    "content": "package constants\n\nconst (\n\tSecretKey         = \"secret_key\"\n\tMachineID         = \"machine_id\"\n\tPolicyGroupGlobal = \"policy_group_global\"\n\tSrcIPConfig       = \"src_ip_config\"\n)\n"
  },
  {
    "path": "management/webserver/pkg/constants/session.go",
    "content": "package constants\n\nconst (\n\tDefaultSessionUserKey = \"user\"\n)\n"
  },
  {
    "path": "management/webserver/pkg/constants/telemetry.go",
    "content": "package constants\n\nconst (\n\tApplicationJson     = \"application/json\"\n\tTelemetryEntryPoint = \"/telemetry\"\n\tTelemetryId         = \"9e88109c-ebbf-4d82-8a25-f97f2ac5f3c4\"\n\tBehaviour           = \"behaviour\"\n\tFalsePositives      = \"false_positives\"\n\tInstallation        = \"Installation\"\n)\n\nvar Version string\n"
  },
  {
    "path": "management/webserver/pkg/cron/cron.go",
    "content": "package cron\n\nimport (\n\t\"github.com/robfig/cron/v3\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log\"\n)\n\nvar logger = log.GetLogger(\"cron\")\n\nfunc newCronWithSeconds() *cron.Cron {\n\tsecondParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour |\n\t\tcron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)\n\treturn cron.New(cron.WithParser(secondParser), cron.WithChain())\n}\n\nfunc StartCron() error {\n\tcronInstance := newCronWithSeconds()\n\t_, err := cronInstance.AddFunc(SpecUpdatePolicy, CheckAndUpdatePolicy)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcronInstance.Start()\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/cron/update_policy.go",
    "content": "package cron\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n)\n\n// SpecUpdatePolicy http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html\n// Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)\n// every 5 second (starting from 0s)\nconst SpecUpdatePolicy = \"0/5 * * * * ?\"\n\ntype statResponseBody struct {\n\tPolicyVersion string `json:\"policy_version\"`\n}\n\nfunc getBytecode() ([]byte, error) {\n\tbuff, err := ioutil.ReadFile(config.GlobalConfig.Detector.FslBytecode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn buff, nil\n}\n\nfunc CheckAndUpdatePolicy() {\n\tif config.GlobalConfig.Detector.Addr == \"\" {\n\t\treturn\n\t}\n\n\texist, err := utils.FileExist(config.GlobalConfig.Detector.FslBytecode)\n\tif !exist || err != nil {\n\t\treturn\n\t}\n\n\tdata, err := getBytecode()\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\treader := bytes.NewReader(data)\n\n\ttr := &http.Transport{\n\t\tMaxIdleConns:    10,\n\t\tIdleConnTimeout: 30 * time.Second,\n\t}\n\tclient := &http.Client{Transport: tr}\n\n\taddr := config.GlobalConfig.Detector.Addr\n\tstatReq, err := http.NewRequest(http.MethodGet, addr+constants.StatEntrypoint, nil)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\tupdateReq, err := http.NewRequest(http.MethodPost, addr+constants.UpdateEntrypoint, reader)\n\tif err != nil {\n\t\tlogger.Error(err)\n\t\treturn\n\t}\n\n\tupdateReq.Header.Set(\"Content-Type\", constants.ContentType)\n\n\tstatRspData := statResponseBody{}\n\tstatRsp, err := client.Do(statReq)\n\tif err != nil || statRsp.StatusCode != http.StatusOK {\n\t\tlogger.Warn(err)\n\t\treturn\n\t}\n\n\tbody, err := ioutil.ReadAll(statRsp.Body)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &statRspData)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\treturn\n\t}\n\n\tif statRspData.PolicyVersion == constants.DefaultPolicyVersion {\n\t\treturn\n\t}\n\n\terr = statRsp.Body.Close()\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t}\n\n\tlogger.Info(\"Update fsl bytecode\")\n\tupdateRsp, err := client.Do(updateReq)\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t\treturn\n\t}\n\n\tif updateRsp.StatusCode != http.StatusOK {\n\t\tlogger.Warnf(\"%s update policy, return %d\", addr, updateRsp.StatusCode)\n\t}\n\terr = updateRsp.Body.Close()\n\tif err != nil {\n\t\tlogger.Warn(err)\n\t}\n}\n"
  },
  {
    "path": "management/webserver/pkg/database/postgres.go",
    "content": "package database\n\nimport (\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n\t\"gorm.io/gorm/schema\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n)\n\ntype PostgresDB struct {\n\t*gorm.DB\n}\n\nvar (\n\t// db is not designed to be used directly, use method `GetDB` instead.\n\tdb PostgresDB\n)\n\nfunc GetDB() *PostgresDB {\n\treturn &db\n}\n\nfunc InitDB() error {\n\tdbConfig := config.GlobalConfig.DB\n\n\tif dbConfig.URL == \"\" {\n\t\treturn errors.New(\"empty database url\")\n\t}\n\t// URL also works, see https://godoc.org/github.com/lib/pq\n\tvar (\n\t\tgormDB *gorm.DB\n\t\terr    error\n\t)\n\n\tgormConfig := &gorm.Config{\n\t\tNamingStrategy: schema.NamingStrategy{\n\t\t\tTablePrefix:   \"mgt_\", // table name prefix, table for `User` would be `t_users`\n\t\t\tSingularTable: true,   // use singular table name, table for `User` would be `user` with this option enabled\n\t\t},\n\t\tAllowGlobalUpdate: false,\n\t\tLogger:            logger.Default.LogMode(logger.Silent),\n\t}\n\tif dbConfig.LogSQL {\n\t\tgormConfig.Logger = logger.Default.LogMode(logger.Info)\n\t}\n\n\tgormDB, err = gorm.Open(postgres.Open(dbConfig.URL), gormConfig)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdb.DB = gormDB\n\n\treturn nil\n}\n\nfunc (db *PostgresDB) SetDB(gormDB *gorm.DB) {\n\tdb.DB = gormDB\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/action.go",
    "content": "package fsl\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc Goto(m string) string {\n\treturn fmt.Sprintf(\"GOTO %s\", m)\n}\n\nfunc Wheres(items ...string) string {\n\treturn strings.Join(items, \" AND \")\n}\n\nfunc Actions(items ...string) string {\n\treturn strings.Join(items, \", \")\n}\n\nfunc Set(k, t, v string) string {\n\treturn fmt.Sprintf(\"SET %s::%s = %s\", k, t, v)\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/quote.go",
    "content": "package fsl\n\nimport (\n\t\"fmt\"\n)\n\n// see : https://chaitin.cn/patronus/fvm/-/blob/master/src/util/StrUtil.cpp#L18\n\nfunc appendEscapedByte(buf []byte, b byte) []byte {\n\tif b >= 0x20 && b <= 0x7e && b != '\\'' && b != '\"' && b != '\\\\' {\n\t\treturn append(buf, b)\n\t}\n\tswitch b {\n\tcase '\\'':\n\t\treturn append(buf, `\\'`...)\n\tcase '\"':\n\t\treturn append(buf, `\\\"`...)\n\tcase '\\n':\n\t\treturn append(buf, `\\n`...)\n\tcase '\\t':\n\t\treturn append(buf, `\\t`...)\n\tcase '\\r':\n\t\treturn append(buf, `\\r`...)\n\tcase '\\b':\n\t\treturn append(buf, `\\b`...)\n\tcase '\\f':\n\t\treturn append(buf, `\\f`...)\n\tcase '\\\\':\n\t\treturn append(buf, `\\\\`...)\n\tdefault:\n\t\treturn append(buf, fmt.Sprintf(\"\\\\x%x\", b)...)\n\t}\n}\n\nfunc appendQuotedWith(buf []byte, s string, quote byte) []byte {\n\tbuf = append(buf, quote)\n\tfor i := 0; i < len(s); i++ {\n\t\tbuf = appendEscapedByte(buf, s[i])\n\t}\n\tbuf = append(buf, quote)\n\treturn buf\n}\n\nfunc quoteWith(s string, quote byte) string {\n\treturn string(appendQuotedWith(make([]byte, 0, 3*len(s)/2), s, quote))\n}\n\nfunc Quote(s string) string {\n\treturn quoteWith(s, '\\'')\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/selector.go",
    "content": "package fsl\n\nimport (\n\t\"fmt\"\n)\n\ntype Selector struct {\n\tTableName string\n\tID        string\n\tWhere     string\n\tAction    string\n}\n\nfunc NewSelector(tableName, id, where, action string) *Selector {\n\treturn &Selector{\n\t\tTableName: tableName,\n\t\tID:        id,\n\t\tWhere:     where,\n\t\tAction:    action,\n\t}\n}\n\nfunc (s *Selector) String() string {\n\tret := fmt.Sprintf(\"ID %s\", Quote(s.ID))\n\tif s.Where != \"\" {\n\t\tret += \" WHERE \" + s.Where\n\t}\n\tret += fmt.Sprintf(\" ACTION %s\", s.Action)\n\treturn ret\n}\n\nfunc (s *Selector) AppendInto() string {\n\treturn fmt.Sprintf(\"APPEND INTO %s %s;\", s.TableName, s.String())\n}\n\nfunc AppendInto(tableName, id, where, action string) string {\n\treturn NewSelector(tableName, id, where, action).AppendInto()\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/state.go",
    "content": "package fsl\n\nimport (\n\t\"chaitin.cn/dev/go/errors\"\n)\n\ntype State int\n\nconst (\n\tS_NOP State = iota\n\tS_ABORT\n\tS_RETURN\n)\n\nfunc (s State) String() string {\n\tswitch s {\n\tcase S_NOP:\n\t\treturn \"NOP\"\n\tcase S_ABORT:\n\t\treturn \"ABORT\"\n\tcase S_RETURN:\n\t\treturn \"RETURN\"\n\tdefault:\n\t\tpanic(errors.New(\"wrong State\"))\n\t}\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/table.go",
    "content": "package fsl\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Table struct {\n\tName     string\n\tMappings map[State]State\n}\n\nfunc NewTable(name string, mappings map[State]State) *Table {\n\treturn &Table{\n\t\tName:     name,\n\t\tMappings: mappings,\n\t}\n}\n\nfunc (t *Table) String() string {\n\tret := fmt.Sprintf(\"TABLE %s\", t.Name)\n\tif len(t.Mappings) > 0 {\n\t\tvar mappings []string\n\t\tfor from, to := range t.Mappings {\n\t\t\tmappings = append(mappings, fmt.Sprintf(\"%s TO %s\", from.String(), to.String()))\n\t\t}\n\t\tret += fmt.Sprintf(\" MAPPING %s\", strings.Join(mappings, \", \"))\n\t}\n\treturn ret\n}\n\nfunc (t *Table) Create() string {\n\treturn fmt.Sprintf(\"CREATE %s;\", t.String())\n}\n\nfunc CreateTable(name string, mappings map[State]State) string {\n\treturn NewTable(name, mappings).Create()\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fsl/target.go",
    "content": "package fsl\n\nimport (\n\t\"fmt\"\n)\n\ntype Target struct {\n\tID   string\n\tType string\n\tArgs string\n}\n\nfunc NewTarget(id string, typ string, args string) *Target {\n\treturn &Target{\n\t\tID:   id,\n\t\tType: typ,\n\t\tArgs: args,\n\t}\n}\n\nfunc NewSkynetTarget(id, configJSONStr string) *Target {\n\treturn NewTarget(id, \"skynet\", configJSONStr)\n}\n\nfunc (t *Target) String() string {\n\treturn fmt.Sprintf(\"%s TYPE %s ARGS (%s)\", t.ID, t.Type, t.Args)\n}\n\nfunc (t *Target) Create() string {\n\treturn fmt.Sprintf(\"CREATE TARGET %s;\", t.String())\n}\n\nfunc CreateTarget(id, configJSONStr string) string {\n\treturn NewSkynetTarget(id, configJSONStr).Create()\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/fvm.go",
    "content": "package fvm\n\n/*\n#cgo CFLAGS: -I../../submodule/fvm/include -I../../submodule/libct/include\n#cgo LDFLAGS: -L../../submodule/fvm/lib -lfvm\n#include <stdlib.h>\n#include <ct/string.h>\n#include <fvm-c/update.h>\n#include <fvm-c/plot.h>\n#include <fvm-c/compiler.h>\n#include <fvm-c/framework.h>\n\nstatic inline fvm_update_t *build_update(const void* buf, size_t len) {\n    ct_string_t tmp = CT_STRING_FROM_PTR_LENGTH(buf, len);\n    return fvm_update_create(tmp);\n}\n*/\nimport (\n\t\"C\" //nolint:typecheck\n)\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"unsafe\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/dev/go/log\"\n)\n\n// Constant for Output\nconst (\n\tOutputItemTable    int = 0\n\tOutputItemSelector int = 1\n\tOutputItemTarget   int = 2\n)\n\n// FVM wrapper\ntype FVM struct {\n\tframework *C.fvm_framework_t\n\tapiTable  *C.char\n\tversion   int64\n}\n\ntype FVMUpdate struct {\n\tupdate *C.fvm_update_t\n}\n\ntype FVMRe struct {\n\tdata []byte\n}\n\nfunc maybePointer(array []byte) unsafe.Pointer {\n\tif len(array) == 0 {\n\t\treturn nil\n\t}\n\treturn unsafe.Pointer(&array[0])\n}\n\nfunc (u *FVMUpdate) ToBytes() []byte {\n\treturn C.GoBytes(unsafe.Pointer(u.update.buf.ptr), C.int(u.update.buf.length))\n}\n\nfunc (u *FVMUpdate) FromBytes(out []byte) error {\n\tptr := C.build_update(unsafe.Pointer(&out[0]), C.ulong(len(out)))\n\tif ptr == nil {\n\t\treturn errors.New(\"failed to create update from db\")\n\t}\n\tu.update = ptr\n\treturn nil\n}\n\nfunc (u *FVMUpdate) MergeUpdate(patch *FVMUpdate) error {\n\tm := C.fvm_update_merge(u.update, patch.update)\n\tif m == nil {\n\t\treturn errors.New(\"failed to merge two updates\")\n\t}\n\tC.fvm_update_destroy(u.update)\n\tu.update = m\n\treturn nil\n}\n\nfunc (u *FVMUpdate) Release() {\n\tC.fvm_update_destroy(u.update)\n}\n\nfunc (r *FVMRe) ToBytes() []byte {\n\treturn r.data\n}\n\nfunc (r *FVMRe) FromBytes(o []byte) {\n\tr.data = o\n}\n\ntype FVMOutput struct {\n\toutput *C.fvm_output_t\n}\n\n// New creates FVM\nfunc New() (*FVM, error) {\n\tapiTable := C.fvm_api_table()\n\n\tfw := C.fvm_framework_acquire(nil)\n\tif fw == nil {\n\t\treturn nil, errors.New(\"fvm_framework_acquire returns a null pointer\")\n\t}\n\n\treturn &FVM{framework: fw, apiTable: apiTable, version: 0}, nil\n}\n\nfunc (f *FVM) Update(upd *FVMUpdate) error {\n\tif upd == nil || upd.update == nil {\n\t\treturn errors.New(\"null pointer passed to update\")\n\t}\n\tret := bool(C.fvm_update(f.framework, upd.update))\n\tif !ret {\n\t\treturn errors.New(\"update framework failed\")\n\t}\n\treturn nil\n}\n\nfunc (f *FVM) Compile(text string, re []byte) (*FVMOutput, *FVMRe, error) {\n\tcplPtr := C.fvm_compiler_acquire(f.apiTable)\n\tcText := C.CString(text)\n\n\tdefer C.fvm_compiler_release(cplPtr)\n\tdefer C.free(unsafe.Pointer(cText))\n\n\tok := C.fvm_compiler_load_re(cplPtr, (*C.char)(maybePointer(re)), C.ulong(len(re)))\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"failed to load re\")\n\t}\n\n\toutputPtr := C.fvm_compile(cplPtr, cText)\n\tif outputPtr == nil {\n\t\treturn nil, nil, errors.New(\"compiling FSL failed\")\n\t}\n\n\tvar cRePtr *C.char\n\tvar cReLen C.ulong\n\tok = C.fvm_compiler_dump_re(cplPtr, &cRePtr, &cReLen)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"failed to dump re\")\n\t}\n\tdefer C.free(unsafe.Pointer(cRePtr))\n\n\tcRe := &FVMRe{C.GoBytes(unsafe.Pointer(cRePtr), C.int(cReLen))}\n\n\treturn &FVMOutput{outputPtr}, cRe, nil\n}\n\nfunc (f *FVM) Plot() string {\n\tplotP := C.fvm_plot(f.framework)\n\tdefer C.fvm_plot_destroy(plotP)\n\tgoBuf := C.GoBytes(unsafe.Pointer(plotP.buf.ptr), C.int(plotP.buf.length))\n\treturn string(goBuf)\n}\n\nfunc (f *FVM) Dump() *FVMUpdate {\n\treturn &FVMUpdate{C.fvm_dump(f.framework)}\n}\n\nfunc (f *FVM) Release() {\n\tC.fvm_framework_release(f.framework)\n}\n\nfunc Serialize(out *FVMOutput, diff bool, base int64, target int64) *FVMUpdate {\n\tupdatePtr := C.fvm_serialize(out.output, C.bool(diff), C.long(base), C.long(target))\n\tif updatePtr == nil {\n\t\treturn nil\n\t}\n\tctStrPtr := updatePtr.buf.ptr\n\tif ctStrPtr == nil {\n\t\treturn nil\n\t}\n\treturn &FVMUpdate{updatePtr}\n}\n\nfunc ReleaseOutput(out *FVMOutput) {\n\tC.fvm_output_destroy(out.output)\n}\n\nfunc (f *FVM) PushFsl(server string, update *FVMUpdate) error {\n\tlog.Infof(\"Push FSL to %s\", server)\n\n\tvar length = uint32(update.update.buf.length)\n\tvar p = uintptr(unsafe.Pointer(update.update.buf.ptr))\n\n\tlog.Infof(\"Get update length %d, %d\", length, C.int(update.update.buf.length))\n\n\tu := &upload{\n\t\tLength: length,\n\t\tP:      p,\n\t}\n\n\tbase, _ := url.Parse(server)\n\tbase.Path = \"/update/policy\"\n\tsnURL := base.String()\n\treq, err := http.NewRequest(\n\t\t\"POST\",\n\t\tsnURL,\n\t\tu,\n\t)\n\tif err != nil {\n\t\treturn errors.Annotatef(err, \"create request failed\")\n\t}\n\treq.Header.Add(\"Content-Type\", \"application/octet-stream\")\n\t// req.Header.Set(\"Connection\", \"close\")\n\n\thttpClient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t},\n\t}\n\n\tvar (\n\t\trespErr error\n\t\tresp    *http.Response\n\t)\n\tfor i := 0; i < 3; i++ {\n\t\tresp, respErr = httpClient.Do(req)\n\t\tif respErr == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif respErr != nil {\n\t\treturn errors.Annotatef(respErr, \"Get response failed\")\n\t}\n\tif resp != nil && resp.StatusCode != 200 {\n\t\tbodyBytes, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tbodyString := string(bodyBytes)\n\n\t\treturn errors.New(fmt.Sprintf(\"Push FSL response(%d %s)\", resp.StatusCode, bodyString))\n\t}\n\n\treturn nil\n}\n\ntype upload struct {\n\tLength uint32\n\toff    uint32\n\tP      uintptr\n}\n\nfunc (u *upload) Read(p []byte) (n int, err error) {\n\tif u.off >= u.Length {\n\t\tif len(p) == 0 {\n\t\t\treturn 0, nil\n\t\t}\n\t\treturn 0, io.EOF\n\t}\n\tplen := uint32(len(p))\n\tvar i uint32\n\tfor i = 0; i < plen; i++ {\n\t\tif i+u.off >= u.Length {\n\t\t\tbreak\n\t\t}\n\t\tp[i] = *(*byte)(unsafe.Pointer(u.P + unsafe.Sizeof(p[0])*uintptr(i+u.off)))\n\t}\n\tu.off = i + u.off\n\treturn int(i), nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/generator.go",
    "content": "package fvm\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"gorm.io/gorm\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/dev/go/log\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/fvm/fsl\"\n)\n\nconst (\n\tMain              = \"main\"\n\tPreprocess        = \"preprocess\"\n\tMassPackage       = \"mass_package\"\n\tPolicyRule        = \"policy_rule\"\n\tPolicyRuleGlobal  = \"policy_rule_global\"\n\tPolicyGroup       = \"policy_group\"\n\tPolicyGroupDetect = \"policy_group_detect\"\n\tWeblogGenerate    = \"weblog_generate\"\n\tReqWeblogGenerate = \"req_weblog_generate\"\n\tRspWeblogGenerate = \"rsp_weblog_generate\"\n\tCommonLog         = \"common_log\"\n\n\tSTR  = \"STRING\"\n\tINT  = \"INTEGER\"\n\tINET = \"INET\"\n\tBOOL = \"BOOLEAN\"\n)\n\nvar logger = log.GetLogger(\"fvm\")\n\nfunc InitFVMBytecode() {\n\tgo func() {\n\t\tfullFSL, err := GenerateFullFSL(database.GetDB().DB)\n\t\tif err != nil {\n\t\t\tlogger.Fatalln(\"Failed to generate fsl\", err)\n\t\t}\n\t\tif err = CompileAndSave(fullFSL); err != nil {\n\t\t\tlogger.Fatalln(\"Failed to compile and save fsl\", err)\n\t\t}\n\t}()\n}\n\nfunc PushFSL(tx *gorm.DB) error {\n\tfullFSL, err := GenerateFullFSL(tx)\n\tif err != nil {\n\t\tlogger.Debugf(\"Full fsl: %s\", fullFSL)\n\t\tlogger.Error(err)\n\t\treturn err\n\t}\n\tif err = CompileAndPush(fullFSL, config.GlobalConfig.Detector.Addr); err != nil {\n\t\tlogger.Error(err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc GenerateFullFSL(tx *gorm.DB) (fullFSL string, err error) {\n\tlogger.Info(\"Generate FSL\")\n\n\tpFSL, err := PreprocessTable(tx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfullFSL += pFSL\n\n\tfullFSL += MassPackageTable()\n\n\tprFSL, err := PolicyRuleTable(tx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfullFSL += prFSL\n\n\tpgFSL, err := PolicyGroupTable(tx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfullFSL += pgFSL\n\n\tfullFSL += WeblogGenerateTable()\n\n\tfullFSL += MainTable()\n\n\treturn fullFSL, err\n}\n\nfunc PreprocessTable(db *gorm.DB) (pFSL string, err error) {\n\tpFSL += fsl.CreateTable(Preprocess, nil)\n\tpFSL += fsl.AppendInto(Preprocess, \"http_parse\", \"\", \"http_parse()\")\n\tpFSL += fsl.AppendInto(Preprocess, \"init_variables\", \"\", fsl.Actions(\n\t\tfsl.Set(\"@site_uuid\", STR, \"get_http_all_headers('SL-CE-SUID')\"),\n\t\tfsl.Set(\"@src_ip\", INET, \"socket.src_ip\"),\n\t\tfsl.Set(\"@skip_remaining\", BOOL, \"false\"),\n\t\tfsl.Set(\"@rule_id\", STR, fsl.Quote(\"\")),\n\t\tfsl.Set(\"@attack_type\", INT, \"-1\"),\n\t\tfsl.Set(\"@final_action\", INT, \"0\"),\n\t\tfsl.Set(\"@risk_level\", INT, \"0\"),\n\t\tfsl.Set(\"@host\", STR, \"http.decayed_host\"),\n\t\tfsl.Set(\"@start_time\", INT, \"now.timestamp_us\"),\n\t\tfsl.Set(\"@transport_scheme\", STR, \"transport.scheme\"),\n\t\tfsl.Set(\"@forward_log\", INT, \"0\"),\n\t\tfsl.Set(\"@convert_to_alog\", INT, \"1\"),\n\t\tfsl.Set(\"@alog_save_to_db\", INT, \"0\"),\n\t\tfsl.Set(\"@convert_to_blog\", INT, \"0\"),\n\t\tfsl.Set(\"@blog_save_to_db\", INT, \"0\"),\n\t\tfsl.Set(\"@convert_to_dlog\", INT, \"0\"),\n\t\tfsl.Set(\"@dlog_save_to_db\", INT, \"0\"),\n\t))\n\tpFSL += fsl.AppendInto(Preprocess, \"set_host\", \"@host::STRING = ''\", fsl.Actions(fsl.Set(\"@host\", STR, \"inet_to_string(socket.dst_ip)\")))\n\tpFSL += fsl.AppendInto(Preprocess, \"adapt_http2\", \"@transport_scheme::STRING = 'http2'\", fsl.Actions(fsl.Set(\"@transport_scheme\", STR, fsl.Quote(\"https\"))))\n\n\t// get src_ip\n\tsc, err := model.GetSrcIPConfig(db)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif sc.Source == model.HTTPHeader {\n\t\tkey := fmt.Sprintf(\"string_to_inet(get_http_all_headers(%s))\", fsl.Quote(sc.Value))\n\t\tpFSL += fsl.AppendInto(Preprocess, \"set_src_ip\", fmt.Sprintf(\"%s != '::/0'::INET\", key), fsl.Actions(fsl.Set(\"@src_ip\", INET, key)))\n\t}\n\treturn pFSL, nil\n}\n\nfunc MassPackageTable() (mpFSL string) {\n\tmpFSL += fsl.CreateTable(MassPackage, nil)\n\tmpFSL += fsl.AppendInto(MassPackage, \"mass_package_detect\", \"string.length(http.raw_req_body) > 1048576\", fsl.Actions(\n\t\tfsl.Set(\"@skip_remaining\", BOOL, \"true\"),\n\t\tfsl.Set(\"@rule_id\", STR, fsl.Quote(\"mass_package\")),\n\t\tfsl.Set(\"@final_action\", INT, \"0\"),\n\t\tfsl.Set(\"@convert_to_dlog\", INT, \"1\"),\n\t\tfsl.Set(\"@dlog_save_to_db\", INT, \"1\"),\n\t\tfsl.Set(\"@attack_type\", INT, \"-4\"),\n\t))\n\treturn mpFSL\n}\n\nfunc PolicyRuleTable(db *gorm.DB) (prFSL string, err error) {\n\tprFSL += fsl.CreateTable(PolicyRuleGlobal, map[fsl.State]fsl.State{fsl.S_ABORT: fsl.S_RETURN})\n\n\tvar policyRuleList []model.PolicyRule\n\tres := db.Model(&model.PolicyRule{}).Where(&model.PolicyRule{IsEnabled: true}).Order(\"id desc\").Find(&policyRuleList)\n\tif res.Error != nil {\n\t\terr = errors.New(fmt.Sprintf(\"Error occurred when fetch PolicyRule: %s\", res.Error))\n\t}\n\n\tfor _, policyRule := range policyRuleList {\n\n\t\tvar patternBytes []byte\n\t\tpatternBytes, err = policyRule.Pattern.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tvar patterns []model.PolicyRulePattern\n\t\terr = json.Unmarshal(patternBytes, &patterns)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tvar wheres []string\n\t\tfor _, pattern := range patterns {\n\t\t\t// transform key to string\n\t\t\tvar key string\n\t\t\tswitch pattern.K {\n\t\t\tcase model.KeySrcIP:\n\t\t\t\tkey = \"inet_to_string(@src_ip::INET)\"\n\t\t\tcase model.KeyURI:\n\t\t\t\tkey = \"@uri_decoded::STRING\"\n\t\t\tcase model.KeyHost:\n\t\t\t\tkey = \"http.host\"\n\t\t\tdefault:\n\t\t\t\treturn \"\", errors.New(\"wrong Key\")\n\t\t\t}\n\n\t\t\tswitch pattern.Op {\n\t\t\tcase model.OpEq:\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"string.equals_case(%s, %s)\", key, fsl.Quote(pattern.V)))\n\t\t\tcase model.OpMatch:\n\t\t\t\t// . -> \\.\n\t\t\t\t// * -> .*\n\t\t\t\tvalueQuote := strings.ReplaceAll(fsl.Quote(strings.ReplaceAll(pattern.V, \".\", \"\\\\.\")), \"*\", \".*\")\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"pcre_match(%s, %s)\", valueQuote, key))\n\t\t\tcase model.OpCIDR:\n\t\t\t\t// use INET for CIDR compare\n\t\t\t\tkey = \"@src_ip::INET\"\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"%s IN CIDR(%s)\", key, fsl.Quote(pattern.V)))\n\t\t\tcase model.OpHas:\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"string.contains_case(%s, %s)\", key, fsl.Quote(pattern.V)))\n\t\t\tcase model.OpPrefix:\n\t\t\t\tl := len(pattern.V)\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"string.equals_case(string.substr(%s, 0, %d), %s)\", key, l, fsl.Quote(pattern.V)))\n\t\t\tcase model.OpRe:\n\t\t\t\twheres = append(wheres, fmt.Sprintf(\"pcre_match(%s, %s)\", fsl.Quote(pattern.V), key))\n\t\t\tdefault:\n\t\t\t\treturn \"\", errors.New(\"wrong Operator\")\n\t\t\t}\n\t\t}\n\n\t\t// save log only when deny and dry_run\n\t\tlogOption := \"1\"\n\t\tattackType := \"-3\"\n\t\t// DROP or ACCEPT\n\t\tappendix := \"DROP\"\n\t\tif policyRule.Action == 0 {\n\t\t\tlogOption = \"0\"\n\t\t\tattackType = \"-2\"\n\t\t\tappendix = \"set_ctx_allow(), ACCEPT\" // this selector seems no use\n\t\t}\n\t\tprFSL += fsl.AppendInto(PolicyRuleGlobal, fmt.Sprintf(\"%s_%d\", PolicyRuleGlobal, policyRule.ID), fsl.Wheres(wheres...), fsl.Actions(\n\t\t\tfsl.Set(\"@skip_remaining\", BOOL, \"true\"),\n\t\t\tfsl.Set(\"@rule_id\", STR, fsl.Quote(fmt.Sprintf(\"/%s\", policyRule.Comment))),\n\t\t\tfsl.Set(\"@final_action\", INT, fmt.Sprintf(\"%d\", policyRule.Action)),\n\t\t\tfsl.Set(\"@convert_to_dlog\", INT, logOption),\n\t\t\tfsl.Set(\"@dlog_save_to_db\", INT, logOption),\n\t\t\tfsl.Set(\"@attack_type\", INT, attackType),\n\t\t\tappendix,\n\t\t\t// no use in v1.1\n\t\t\t//fsl.Set(\"@risk_level\", INT, \"\"),\n\t\t))\n\t}\n\n\tprFSL += fsl.CreateTable(PolicyRule, nil)\n\tprFSL += fsl.AppendInto(PolicyRule, \"preprocess\", \"\", fsl.Actions(fsl.Set(\"@uri_decoded\", STR, \"url_decode(http.uri)\")))\n\tprFSL += fsl.AppendInto(PolicyRule, \"policy_rule_global\", \"\", fsl.Goto(PolicyRuleGlobal))\n\tprFSL += fsl.AppendInto(PolicyRule, \"set_status_code\", \"@final_action::INTEGER = 1\", \"set_status_code(403)\")\n\n\treturn prFSL, err\n}\n\nfunc PolicyGroupTable(db *gorm.DB) (pgFSL string, err error) {\n\tskynetConfigStr, err := model.GetSkynetConfig(db)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tpgFSL += fsl.CreateTarget(\"skynet_config\", fsl.Quote(skynetConfigStr))\n\n\tpgFSL += fsl.CreateTable(PolicyGroupDetect, map[fsl.State]fsl.State{fsl.S_ABORT: fsl.S_RETURN})\n\tpgFSL += fsl.AppendInto(PolicyGroupDetect, \"do_detect\", \"\", \"target 'skynet_config' type skynet do detect()\")\n\n\tpgFSL += fsl.CreateTable(PolicyGroup, nil)\n\tpgFSL += fsl.AppendInto(PolicyGroup, \"do_detect\", \"\", fsl.Goto(PolicyGroupDetect))\n\tpgFSL += fsl.AppendInto(PolicyGroup, \"dlog_persistence\", \"skynet.enable_log = true\", fsl.Actions(\n\t\tfsl.Set(\"@convert_to_dlog\", INT, \"1\"),\n\t\tfsl.Set(\"@dlog_save_to_db\", INT, \"1\"),\n\t))\n\tpgFSL += fsl.AppendInto(PolicyGroup, \"set_status_code\", \"skynet.action = 1\", \"set_status_code(403)\")\n\tpgFSL += fsl.AppendInto(PolicyGroup, \"hit_policy_group\", \"skynet.rule_id != ''\", fsl.Actions(\n\t\tfsl.Set(\"@skip_remaining\", BOOL, \"true\"),\n\t\tfsl.Set(\"@rule_id\", STR, \"skynet.rule_id\"),\n\t\tfsl.Set(\"@attack_type\", INT, \"skynet.main_attack\"),\n\t\tfsl.Set(\"@final_action\", INT, \"skynet.action\"),\n\t\tfsl.Set(\"@risk_level\", INT, \"skynet.risk_level\"),\n\t\tfsl.Set(\"@convert_to_dlog\", INT, \"1\"),\n\t))\n\treturn pgFSL, nil\n}\n\nfunc CommonLogTable() (clFSL string) {\n\tclFSL += fsl.CreateTable(CommonLog, nil)\n\tclFSL += fsl.AppendInto(CommonLog, \"batch_set_payload_string\", \"\", \"weblog_payload_set_string_batch({'event_id', 'scheme', 'dst_ip', 'socket_ip', 'src_ip'}::ARRAY(STRING), {extra.uuid, transport.scheme, inet_to_string(socket.dst_ip), inet_to_string(socket.src_ip), inet_to_string(@src_ip::INET)}::ARRAY(STRING))\")\n\tclFSL += fsl.AppendInto(CommonLog, \"batch_set_payload_int\", \"\", \"weblog_payload_set_int_batch({'src_port', 'dst_port'}::ARRAY(STRING), {socket.src_port, socket.dst_port}::ARRAY(INTEGER))\")\n\tclFSL += fsl.AppendInto(CommonLog, \"batch_set_extra_bool\", \"\", \"weblog_extra_set_bool_batch({'alog_save_to_db', 'convert_to_blog', 'forward_log', 'convert_to_alog', 'convert_to_dlog', 'dlog_save_to_db', 'blog_save_to_db'}::ARRAY(STRING), {@alog_save_to_db::INTEGER, @convert_to_blog::INTEGER, @forward_log::INTEGER, @convert_to_alog::INTEGER, @convert_to_dlog::INTEGER, @dlog_save_to_db::INTEGER, @blog_save_to_db::INTEGER}::ARRAY(INTEGER))\")\n\tclFSL += fsl.AppendInto(CommonLog, \"generate\", \"\", \"weblog_generate()\")\n\treturn clFSL\n}\n\nfunc ReqWeblogGenerateTable() (reqWGFSL string) {\n\treqWGFSL += fsl.CreateTable(ReqWeblogGenerate, nil)\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"init_http_req_body\", \"\", fsl.Actions(fsl.Set(\"@raw_req_body\", STR, \"http.raw_req_body\")))\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"cut_http_req_body\", \"string.length(@raw_req_body::STRING) > 1048576\", fsl.Actions(fsl.Set(\"@raw_req_body\", STR, \"string.substr(http.raw_req_body, 0, 1048576)\")))\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"batch_set_payload_binary\", \"\", \"weblog_payload_set_binary_batch({'req_payload', 'req_body'}::ARRAY(STRING), {skynet.payload, @raw_req_body::STRING}::ARRAY(STRING))\")\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"batch_set_payload_string\", \"\", \"weblog_payload_set_string_batch({'site_uuid', 'req_header', 'req_detector_name', 'req_proxy_name', 'req_rule_id', 'req_location', 'req_decode_path', 'user_agent', 'query_string', 'method', 'host', 'referer', 'url_path', 'cookie'}::ARRAY(STRING), {@site_uuid::STRING, http.raw_req_header, fusion.node_id, extra.proxy_name, @rule_id::STRING, skynet.location, skynet.decode_path, http.user_agent, http.decoded_query, http.raw_method, @host::STRING, http.referer, http.path, http.raw_cookie}::ARRAY(STRING))\")\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"batch_set_payload_int\", \"\", \"weblog_payload_set_int_batch({'req_start_time', 'req_detect_time', 'req_block_reason', 'req_attack_type', 'req_risk_level', 'req_end_time', 'req_action'}::ARRAY(STRING), {extra.req_start_time, now.timestamp_us - @start_time::INTEGER, fusion.block_reason, @attack_type::INTEGER, @risk_level::INTEGER, extra.req_end_time, @final_action::INTEGER}::ARRAY(INTEGER))\")\n\treqWGFSL += fsl.AppendInto(ReqWeblogGenerate, \"common_log\", \"\", fsl.Goto(CommonLog))\n\treturn reqWGFSL\n}\n\nfunc RspWeblogGenerateTable() (rspWGFSL string) {\n\trspWGFSL += fsl.CreateTable(RspWeblogGenerate, nil)\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"init_http_rsp_body\", \"\", fsl.Actions(fsl.Set(\"@raw_rsp_body\", STR, \"http.raw_rsp_body\")))\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"cut_http_rsp_body\", \"string.length(@raw_rsp_body::STRING) > 1048576\", fsl.Actions(fsl.Set(\"@raw_rsp_body\", STR, \"string.substr(http.raw_rsp_body, 0, 1048576)\")))\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"batch_set_payload_binary\", \"\", \"weblog_payload_set_binary_batch({'rsp_payload', 'rsp_body'}::ARRAY(STRING), {skynet.payload, @raw_rsp_body::STRING}::ARRAY(STRING))\")\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"batch_set_payload_string\", \"\", \"weblog_payload_set_string_batch({'rsp_rule_id', 'rsp_location', 'rsp_decode_path', 'rsp_header', 'rsp_detector_name', 'rsp_proxy_name'}::ARRAY(STRING), {@rule_id::STRING, skynet.location, skynet.decode_path, http.raw_rsp_header, fusion.node_id, extra.proxy_name}::ARRAY(STRING))\")\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"batch_set_payload_int\", \"\", \"weblog_payload_set_int_batch({'rsp_detect_time', 'status_code', 'rsp_start_time', 'rsp_end_time', 'rsp_action', 'rsp_attack_type', 'rsp_risk_level', 'rsp_block_reason'}::ARRAY(STRING), {now.timestamp_us - @start_time::INTEGER, http.status_code, extra.rsp_start_time, extra.rsp_end_time, @final_action::INTEGER, @attack_type::INTEGER, @risk_level::INTEGER, fusion.block_reason}::ARRAY(INTEGER))\")\n\trspWGFSL += fsl.AppendInto(RspWeblogGenerate, \"common_log\", \"\", fsl.Goto(CommonLog))\n\treturn rspWGFSL\n}\n\nfunc WeblogGenerateTable() (wgFSL string) {\n\twgFSL += CommonLogTable()\n\twgFSL += ReqWeblogGenerateTable()\n\twgFSL += RspWeblogGenerateTable()\n\twgFSL += fsl.CreateTable(WeblogGenerate, nil)\n\twgFSL += fsl.AppendInto(WeblogGenerate, \"req\", \"fusion.stage = 0\", fsl.Goto(ReqWeblogGenerate))\n\twgFSL += fsl.AppendInto(WeblogGenerate, \"rsp\", \"fusion.stage = 1\", fsl.Goto(RspWeblogGenerate))\n\treturn wgFSL\n}\n\nfunc MainTable() (mainFSL string) {\n\tmainFSL += fsl.CreateTable(Main, nil)\n\tmainFSL += fsl.AppendInto(Main, \"m_preprocess\", \"\", fsl.Goto(Preprocess))\n\tmainFSL += fsl.AppendInto(Main, \"m_mass_package\", \"\", fsl.Goto(MassPackage))\n\tmainFSL += fsl.AppendInto(Main, \"m_policy_rule\", \"@skip_remaining::BOOLEAN = false\", fsl.Goto(PolicyRule))\n\tmainFSL += fsl.AppendInto(Main, \"m_policy_group\", \"@skip_remaining::BOOLEAN = false\", fsl.Goto(PolicyGroup))\n\tmainFSL += fsl.AppendInto(Main, \"m_weblog_generate\", \"\", fsl.Goto(WeblogGenerate))\n\tmainFSL += \"ENTRYPOINT TABLE main;\"\n\treturn mainFSL\n}\n"
  },
  {
    "path": "management/webserver/pkg/fvm/helper.go",
    "content": "package fvm\n\nimport (\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\nvar GlobalFVM *FVM\n\nfunc init() {\n\tvar err error\n\tGlobalFVM, err = New()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc CompileAndSave(text string) error {\n\toutput, _, err := GlobalFVM.Compile(text, nil)\n\tif err != nil {\n\t\treturn errors.Annotate(err, \"failed to compile rules\")\n\t}\n\tdefer ReleaseOutput(output)\n\n\tupdate := Serialize(output, false, 0, 1)\n\tdefer update.Release()\n\n\t// save to file\n\tif err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil {\n\t\treturn errors.Annotate(err, \"failed to save bytecode\")\n\t}\n\n\treturn nil\n}\n\nfunc CompileAndPush(text, serverAddr string) error {\n\toutput, _, err := GlobalFVM.Compile(text, nil)\n\tif err != nil {\n\t\treturn errors.Annotate(err, \"failed to compile rules\")\n\t}\n\tdefer ReleaseOutput(output)\n\n\tupdate := Serialize(output, false, 0, 1)\n\tdefer update.Release()\n\n\t// save to file\n\tif err = utils.EnsureWriteFile(config.GlobalConfig.Detector.FslBytecode, update.ToBytes(), 0666); err != nil {\n\t\treturn errors.Annotate(err, \"failed to save bytecode\")\n\t}\n\n\tif err := GlobalFVM.PushFsl(serverAddr, update); err != nil {\n\t\treturn errors.Annotatef(err, \"failed to push rule to '%s'\", serverAddr)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/pkg/log/log.go",
    "content": "package log\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"chaitin.cn/dev/go/log\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\nfunc GetLogger(name string) *log.Logger {\n\treturn log.GetLogger(name)\n}\n\nfunc LoadLogLevel() {\n\tlv, _ := log.ParseLevel(config.GlobalConfig.Log.Level)\n\tlog.SetLevel(log.AllLoggers, lv)\n}\n\nfunc SetLogFormatter() {\n\t// format\n\tformatter := new(log.TextFormatter)\n\tformatter.FullTimestamp = true\n\tformatter.TimestampFormat = \"2006/01/02 15:04:05\"\n\tlog.SetFormatter(log.AllLoggers, formatter)\n}\n\nfunc InitLogger() error {\n\t// output\n\tswitch config.GlobalConfig.Log.Output {\n\tcase \"stdout\":\n\t\tlog.SetOutput(log.AllLoggers, os.Stdout)\n\tcase \"stderr\":\n\t\tlog.SetOutput(log.AllLoggers, os.Stderr)\n\tdefault:\n\t\texist, err := utils.FileExist(config.GlobalConfig.Log.Output)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfileFlag := os.O_WRONLY | os.O_APPEND | os.O_SYNC\n\t\tif !exist {\n\t\t\tif err := utils.EnsureFileDir(config.GlobalConfig.Log.Output); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfileFlag = fileFlag | os.O_CREATE\n\t\t}\n\n\t\tif fp, err := os.OpenFile(config.GlobalConfig.Log.Output, fileFlag, os.ModePerm); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open log file: %s\", err.Error())\n\t\t} else {\n\t\t\tlog.SetOutput(log.AllLoggers, log.NewLockOutput(fp))\n\t\t}\n\t}\n\n\t// hook\n\tlog.AddHook(log.AllLoggers, NewRuntimeHook())\n\tlog.AddHook(log.AllLoggers, log.NewErrorStackHook(true))\n\t// level\n\tLoadLogLevel()\n\n\treturn nil\n}\n\ntype RuntimeHook struct{}\n\nfunc (h *RuntimeHook) Levels() []logrus.Level {\n\treturn logrus.AllLevels\n}\n\nfunc (h *RuntimeHook) Fire(entry *logrus.Entry) error {\n\tfile := \"???\"\n\tfuncName := \"???\"\n\tline := 0\n\n\tpc := make([]uintptr, 64)\n\t// Skip runtime.Callers, self, and another call from logrus\n\tn := runtime.Callers(3, pc)\n\tif n != 0 {\n\t\tpc = pc[:n] // pass only valid pcs to runtime.CallersFrames\n\t\tframes := runtime.CallersFrames(pc)\n\n\t\t// Loop to get frames.\n\t\t// A fixed number of pcs can expand to an indefinite number of Frames.\n\t\tfor {\n\t\t\tframe, more := frames.Next()\n\t\t\tif !strings.Contains(frame.File, \"github.com/sirupsen/logrus\") && !strings.Contains(frame.Function, \"chaitin.cn/dev/go\") {\n\t\t\t\tfile = frame.File\n\t\t\t\tfuncName = frame.Function\n\t\t\t\tline = frame.Line\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !more {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tslices := strings.Split(file, \"/\")\n\tfile = slices[len(slices)-1]\n\n\tfuncName = strings.ReplaceAll(funcName, \"chaitin.cn\", \"\")\n\n\tentry.Data[\"file\"] = file\n\tentry.Data[\"func\"] = funcName\n\tentry.Data[\"line\"] = line\n\treturn nil\n}\n\nfunc NewRuntimeHook() *RuntimeHook {\n\treturn &RuntimeHook{}\n}\n"
  },
  {
    "path": "management/webserver/pkg/telemetry.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/utils\"\n)\n\ntype WebsiteResult struct {\n\tId        uint\n\tUpdatedAt time.Time\n}\n\ntype TelemetryInfo struct {\n\tId string `json:\"id\"`\n}\n\ntype SafelineInfo struct {\n\tId string `json:\"id\"`\n\n\tType    string `json:\"type\"`\n\tVersion string `json:\"version\"`\n\n\tReqCnt         int  `json:\"req_cnt\"`\n\tDetectLogCnt   int  `json:\"detect_log_cnt\"`\n\tSiteCnt        int  `json:\"site_cnt\"`\n\tHealthySiteCnt int  `json:\"healthy_site_cnt\"`\n\tRuleCnt        int  `json:\"rule_cnt\"`\n\tIsHealthy      bool `json:\"is_healthy\"`\n\n\tBehavior    map[string]int  `json:\"behavior\"`\n\tWebsites    []WebsiteResult `json:\"websites\"`\n\tWebsitesCnt int             `json:\"websites_cnt\"`\n}\n\ntype TelemetryRequest struct {\n\tTelemetry TelemetryInfo `json:\"telemetry\"`\n\tSafeline  SafelineInfo  `json:\"safeline\"`\n}\n\nfunc GetUploadTimestamp() string {\n\tyesterday := time.Now().AddDate(0, 0, -1)\n\tyesterdayNoon := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 12, 0, 0, 0, yesterday.Location())\n\treturn strconv.FormatInt(yesterdayNoon.Unix(), 10)\n}\n\nfunc GetUploadNonce() string {\n\tnow := strconv.FormatInt(time.Now().Unix(), 10)\n\trandStr := utils.RandStr(6)\n\treturn fmt.Sprintf(\"%s%s\", now, randStr)\n}\n\nfunc DoPostTelemetry(client *http.Client, addr string, reader io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequest(http.MethodPost, \"https://\"+addr+constants.TelemetryEntryPoint, reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"accept\", constants.ApplicationJson)\n\treq.Header.Set(\"Content-Type\", constants.ApplicationJson)\n\n\trsp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rsp, nil\n}\n"
  },
  {
    "path": "management/webserver/proto/website/website.proto",
    "content": "syntax = \"proto3\";\npackage website;\noption go_package = \"proto/website\";\n\nservice Website {\n  rpc Subscribe(stream Response) returns (stream Event) {}\n}\n\n// From client-side, may be \"pong\"\nmessage Response {\n  string type = 1;\n  bytes msg = 2;\n  bool err = 3;\n}\n\n// From server-side, may be \"ping\"\nmessage Event {\n  string type = 1;    // ping/website\n  bytes msg = 2;\n}\n"
  },
  {
    "path": "management/webserver/rpc/main.go",
    "content": "package rpc\n\nimport (\n\t\"net\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/dev/go/log\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/config\"\n\tpb \"chaitin.cn/patronus/safeline-2/management/webserver/proto/website\"\n)\n\nconst (\n\tWaitRspTimeout   = 30 * time.Second\n\tKeepaliveTime    = 5 * time.Second\n\tKeepaliveTimeout = 30 * time.Second\n\n\tPing                   = \"ping\"\n\tPong                   = \"pong\"\n\tEventTypeWebsite       = \"website\"\n\tEventTypeDeleteWebsite = \"deleteWebsite\"\n\tEventTypeFullWebsite   = \"fullWebsite\"\n)\n\nvar logger = log.GetLogger(\"grpc\")\n\nfunc StartGRPCSever() error {\n\tlis, err := net.Listen(\"tcp\", config.GlobalConfig.GPRC.ListenAddr)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"Failed to listen\")\n\t}\n\tvar opts []grpc.ServerOption\n\tgrpcServer := grpc.NewServer(opts...)\n\tpb.RegisterWebsiteServer(grpcServer, GetWebsiteServer())\n\tgo func() {\n\t\terr := grpcServer.Serve(lis)\n\t\tif err != nil {\n\t\t\tlogger.Fatalln(\"Failed to server\")\n\t\t}\n\t}()\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/rpc/website.go",
    "content": "package rpc\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"time\"\n\n\t\"chaitin.cn/dev/go/errors\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/model\"\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database\"\n\tpb \"chaitin.cn/patronus/safeline-2/management/webserver/proto/website\"\n)\n\nvar (\n\tping       = pb.Event{Type: Ping, Msg: nil}\n\tSubscriber *StreamClient // only ONE sub\n)\n\nfunc Publish(msg []byte, eventType string) error {\n\tif Subscriber == nil {\n\t\treturn errors.New(\"Service is abnormal, and the nginx conf cannot be updated for the time being. Please go to the shell to check the relevant logs\")\n\t}\n\terr := Subscriber.stream.Send(&pb.Event{\n\t\tType: eventType,\n\t\tMsg:  msg,\n\t})\n\tif err != nil {\n\t\tselect {\n\t\tcase <-Subscriber.stream.Context().Done():\n\t\t\terr = Subscriber.stream.Context().Err() // context canceled\n\t\t\tSubscriber = nil\n\t\t\treturn err\n\t\tcase Subscriber.errCh <- errors.Wrapf(err, \"Send event err: %s\", msg):\n\t\t\treturn errors.Wrapf(err, \"Send event err: %s\", msg)\n\t\t}\n\t}\n\n\trspTimeoutTicker := time.NewTicker(WaitRspTimeout)\n\tdefer rspTimeoutTicker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase rsp := <-Subscriber.rspCh:\n\t\t\tif rsp.Err {\n\t\t\t\treturn errors.New(string(rsp.Msg))\n\t\t\t} else {\n\t\t\t\t// success\n\t\t\t\treturn nil\n\t\t\t}\n\t\tcase <-rspTimeoutTicker.C:\n\t\t\treturn errors.New(\"Wait timeout for updating result\")\n\t\t}\n\t}\n}\n\n// StreamClient is the instance for every client stream connected\ntype StreamClient struct {\n\tstream pb.Website_SubscribeServer\n\ttimer  *time.Timer // timer for timeout\n\terrCh  chan error\n\trspCh  chan *pb.Response\n\tquit   chan struct{} // quit stream client gracefully\n}\n\nfunc newStreamClient(stream pb.Website_SubscribeServer) *StreamClient {\n\treturn &StreamClient{\n\t\tstream: stream,\n\t\ttimer:  time.NewTimer(KeepaliveTimeout),\n\t\terrCh:  make(chan error, 1),\n\t\trspCh:  make(chan *pb.Response, 1),\n\t\tquit:   make(chan struct{}),\n\t}\n}\n\nfunc (sc *StreamClient) pingLoop() {\n\tpingTicker := time.NewTicker(KeepaliveTime)\n\tdefer pingTicker.Stop()\n\n\tfor {\n\t\tif Subscriber != nil && sc != Subscriber {\n\t\t\t// new subscriber in replace of the old one.\n\t\t\tlogger.Debug(\"New subscriber in replace of the old one\")\n\t\t\treturn\n\t\t}\n\n\t\tselect {\n\t\tcase <-sc.stream.Context().Done():\n\t\t\treturn\n\t\tcase <-pingTicker.C:\n\t\t\terr := sc.stream.Send(&ping)\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-sc.stream.Context().Done():\n\t\t\t\tcase sc.errCh <- errors.Wrapf(err, \"Send ping err\"):\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (sc *StreamClient) recvLoop() {\n\tfor {\n\t\tif Subscriber != nil && sc != Subscriber {\n\t\t\t// new subscriber in replace of the old one.\n\t\t\tclose(sc.quit)\n\t\t\tlogger.Debug(\"New subscriber in replace of the old one\")\n\t\t\treturn\n\t\t}\n\n\t\trsp, err := sc.stream.Recv()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tselect {\n\t\t\t\tcase <-sc.stream.Context().Done():\n\t\t\t\tcase sc.errCh <- errors.Wrapf(err, \"Recv pong err\"):\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tlogger.Debugf(\"Got message Type %s\", rsp.GetType())\n\t\tif rsp.Type != Pong {\n\t\t\t// receive response\n\t\t\tlogger.Infof(\"Recv updating website rsp: err(%t), msg(%s)\", rsp.GetErr(), rsp.GetMsg())\n\t\t\tsc.rspCh <- rsp\n\t\t\tcontinue\n\t\t}\n\t\tsc.timer.Reset(KeepaliveTimeout)\n\t}\n}\n\n// WebsiteServer is the gRPC server implementation\ntype WebsiteServer struct {\n\t*pb.UnimplementedWebsiteServer\n}\n\nfunc GetWebsiteServer() *WebsiteServer {\n\treturn &WebsiteServer{}\n}\n\nfunc publishFullWebsite() error {\n\tvar websites []model.Website\n\tdb := database.GetDB().DB\n\tdb.Model(&model.Website{}).Find(&websites)\n\tbyteWebsites, err := json.Marshal(&websites)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn Publish(byteWebsites, EventTypeFullWebsite)\n}\n\n// Subscribe is gRPC API entrypoint\nfunc (ws *WebsiteServer) Subscribe(stream pb.Website_SubscribeServer) error {\n\tSubscriber = newStreamClient(stream)\n\tdefer Subscriber.timer.Stop()\n\n\tgo Subscriber.pingLoop()\n\tgo Subscriber.recvLoop()\n\n\tif err := publishFullWebsite(); err != nil {\n\t\t// triggered when tcd starts, ignore push error messages\n\t\tlogger.Warn(err)\n\t}\n\n\tselect {\n\tcase <-Subscriber.quit:\n\t\tlogger.Infof(\"Disconnected gracefully\")\n\t\treturn nil\n\tcase <-Subscriber.timer.C:\n\t\tlogger.Error(\"Keepalive timeout\")\n\tcase err := <-Subscriber.errCh:\n\t\tlogger.WithError(err).Error()\n\t\treturn err\n\tcase <-Subscriber.stream.Context().Done():\n\t\tlogger.Infof(\"Subscribe context done: %s\", Subscriber.stream.Context().Err())\n\t\tSubscriber = nil\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "management/webserver/tools/init_db.sh",
    "content": "docker rm -f mgt-postgres-dev\n\n#docker run -it -d -e POSTGRES_DB=safeline-ce -e POSTGRES_USER=safeline-ce -e POSTGRES_PASSWORD=safeline-ce -p 127.0.0.1:5432:5432 --name mgt-postgres-dev chaitin.cn/library/postgres:15.2\ndocker run -it -d -e POSTGRES_DB=safeline-ce -e POSTGRES_USER=safeline-ce -e POSTGRES_PASSWORD=safeline-ce -p 127.0.0.1:5432:5432 --name mgt-postgres-dev postgres:15.2"
  },
  {
    "path": "management/webserver/utils/cert.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"io/ioutil\"\n\t\"math/big\"\n\t\"time\"\n)\n\nfunc GenerateCert(hostnames []string, days int64, keyBits int, subject *pkix.Name, isCA bool) ([]byte, []byte, error) {\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, keyBits)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tnotBefore := time.Now()\n\tduration := int64(time.Hour) * 24 * days\n\tnotAfter := notBefore.Add(time.Duration(duration))\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar keyUsage x509.KeyUsage\n\tif isCA {\n\t\tkeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign\n\t} else {\n\t\tkeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber:          serialNumber,\n\t\tNotBefore:             notBefore,\n\t\tNotAfter:              notAfter,\n\t\tDNSNames:              hostnames,\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  isCA,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},\n\t\tKeyUsage:              keyUsage,\n\t}\n\tif subject != nil {\n\t\ttemplate.Subject = *subject\n\t}\n\n\tderBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar certBuffer bytes.Buffer\n\terr = pem.Encode(&certBuffer, &pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar keyBuffer bytes.Buffer\n\terr = pem.Encode(&keyBuffer, &pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn certBuffer.Bytes(), keyBuffer.Bytes(), nil\n}\n\nfunc WriteCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) error {\n\tcert, key, err := genCertIfNotExist(certFilePath, keyFilePath, generator)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = EnsureRenameWriteFile(certFilePath, cert, 0644); err != nil {\n\t\treturn err\n\t}\n\n\tif err = EnsureRenameWriteFile(keyFilePath, key, 0644); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc genCertIfNotExist(certFilePath, keyFilePath string, generator func() ([]byte, []byte, error)) ([]byte, []byte, error) {\n\texist, err := FilesExist(certFilePath, keyFilePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif !exist {\n\t\treturn generator()\n\t} else {\n\t\tcert, err := ioutil.ReadFile(certFilePath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tkey, err := ioutil.ReadFile(keyFilePath)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn cert, key, nil\n\t}\n}\n"
  },
  {
    "path": "management/webserver/utils/file.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc EnsureDir(dir string) error {\n\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\treturn os.MkdirAll(dir, os.FileMode(0755))\n\t}\n\treturn nil\n}\n\nfunc EnsureFileDir(path string) error {\n\treturn EnsureDir(filepath.Dir(path))\n}\n\nfunc FileExist(path string) (bool, error) {\n\tstat, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t} else {\n\t\t\treturn false, err\n\t\t}\n\t} else {\n\t\tif stat.IsDir() {\n\t\t\treturn false, fmt.Errorf(\"%s is dir\", path)\n\t\t} else {\n\t\t\treturn true, nil\n\t\t}\n\t}\n}\n\nfunc FilesExist(paths ...string) (bool, error) {\n\tfor _, path := range paths {\n\t\texist, err := FileExist(path)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\nfunc RenameWriteFile(filename string, data []byte, perm os.FileMode) error {\n\trandFileName := filename + \".tmp.\" + RandStr(8)\n\tif err := ioutil.WriteFile(randFileName, data, perm); err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(randFileName, filename)\n}\n\nfunc EnsureRenameWriteFile(path string, data []byte, mode os.FileMode) error {\n\terr := EnsureFileDir(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn RenameWriteFile(path, data, mode)\n}\n\nfunc EnsureWriteFile(path string, data []byte, mode os.FileMode) error {\n\terr := EnsureFileDir(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn ioutil.WriteFile(path, data, mode)\n}\n"
  },
  {
    "path": "management/webserver/utils/healthy.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc CheckHealthy(url string) bool {\n\tmethod := \"GET\"\n\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(method, url, nil)\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn false\n\t}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn false\n\t}\n\tdefer res.Body.Close()\n\n\treturn res.StatusCode == 200\n}\n\nfunc CheckWafHealthy() bool {\n\t// todo: not check the healthy status\n\treturn CheckHealthy(\"http://safeline-mario:3335\") && CheckHealthy(\"http://safeline-detector:8001/stat\")\n}\n"
  },
  {
    "path": "management/webserver/utils/httpclient.go",
    "content": "package utils\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n)\n\nconst proxyName = \"HTTPS_PROXY\"\n\nvar httpClient *http.Client\n\nfunc GetHTTPClient() *http.Client {\n\tif httpClient == nil {\n\t\ttr := &http.Transport{\n\t\t\tMaxIdleConns:    10,\n\t\t\tIdleConnTimeout: 30 * time.Second,\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: true,\n\t\t\t},\n\t\t}\n\n\t\tproxyUrl, existed := os.LookupEnv(proxyName)\n\t\tif existed {\n\t\t\turi, _ := url.Parse(proxyUrl)\n\t\t\ttr.Proxy = http.ProxyURL(uri)\n\t\t}\n\n\t\thttpClient = &http.Client{Transport: tr}\n\t}\n\n\treturn httpClient\n}\n"
  },
  {
    "path": "management/webserver/utils/random.go",
    "content": "package utils\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n)\n\nvar letters = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\nfunc RandStr(n int) string {\n\trand.Seed(time.Now().UnixNano())\n\n\tb := make([]rune, n)\n\tfor i := range b {\n\t\tb[i] = letters[rand.Intn(len(letters))]\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "management/webserver/utils/url.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants\"\n)\n\nfunc IsIPv6(str string) bool {\n\tip := net.ParseIP(str)\n\treturn ip != nil && strings.Contains(str, \":\")\n}\n\nfunc BuildUrl(protocol int, host string, port uint, path string) string {\n\tportStr := \"\"\n\tif len(host) > 0 && IsIPv6(host) && host[:1] != \"[\" {\n\t\thost = fmt.Sprintf(\"[%s]\", host)\n\t}\n\tif protocol == constants.ProtocolHTTP {\n\t\tif port != 80 {\n\t\t\tportStr = fmt.Sprintf(\":%d\", port)\n\t\t}\n\t} else if protocol == constants.ProtocolHTTPS || protocol == constants.ProtocolHTTP2 {\n\t\tif port != 443 {\n\t\t\tportStr = fmt.Sprintf(\":%d\", port)\n\t\t}\n\t} else {\n\t\t// use HTTP as default protocol\n\t\tprotocol = constants.ProtocolHTTP\n\t}\n\treturn fmt.Sprintf(\"%s://%s%s%s\", constants.HTTPProtocol[protocol], host, portStr, path)\n}\n"
  },
  {
    "path": "mcp_server/Dockerfile",
    "content": "FROM golang:1.24-alpine AS builder\n\nWORKDIR /app\n\nRUN apk add --no-cache git\n\nCOPY go.mod go.sum ./\n\nRUN go mod download\n\nCOPY . .\n\nRUN CGO_ENABLED=0 go build -o mcp-server .\n\nFROM alpine:latest\n\nWORKDIR /app\n\nRUN apk add --no-cache ca-certificates tzdata\n\nCOPY --from=builder /app/mcp-server .\nCOPY --from=builder /app/config.yaml .\n\nENV TZ=Asia/Shanghai\n\nEXPOSE 5678\n\nCMD [\"./mcp-server\"] "
  },
  {
    "path": "mcp_server/README.md",
    "content": "# SafeLine MCP Server\n\nSafeLine MCP Server is an implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that provides complete management and control capabilities for SafeLine WAF.\n\n[![Docker](https://img.shields.io/badge/Docker-Supported-2496ED?style=flat-square&logo=docker&logoColor=white)](docker-compose.yml)\n[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod)\n\n## Use Cases\n\n- Automated management and control of SafeLine WAF instances\n- WAF configuration and policy management through API\n- Building AI-based security protection tools and applications\n\n## Prerequisites\n\n1. Install [Docker](https://www.docker.com/) (if running in container)\n2. Configure SafeLine API Token (obtained from SafeLine console)\n\n## Features\n\n- Complete MCP (Management Control Protocol) server implementation\n- Support for SafeLine WAF instance management and control\n- Flexible configuration system supporting file configuration and environment variables\n- Docker containerization support\n- Secure API communication\n\n## Quick Start\n\n### Environment Variables\n\n| Environment Variable | Description | Default Value | Required |\n|---------|------|--------|-----|\n| LISTEN_PORT | Service listening port | 5678 | No |\n| LISTEN_ADDRESS | Service listening address | 0.0.0.0 | No |\n| SAFELINE_SECRET | SSE server secret | - | No |\n| SAFELINE_ADDRESS | SafeLine API address | - | Yes |\n| SAFELINE_API_TOKEN | SafeLine API authentication token | - | Yes |\n\n### Using Docker\n\n#### Method 1: Using docker run\n\n```bash\ndocker run -d \\\n  --name safeline-mcp \\\n  -p 5678:5678 \\\n  -e SAFELINE_API_TOKEN=\"your_api_token\" \\\n  -e SAFELINE_ADDRESS=\"https://your.safeline.com\" \\\n  -e LISTEN_PORT=5678 \\\n  -e LISTEN_ADDRESS=\"0.0.0.0\" \\\n  chaitin/safeline-mcp:latest\n```\n\n#### Method 2: Using docker-compose\n\n```bash\n# 1. Clone repository\ngit clone https://github.com/chaitin/safeline-mcp.git\ncd safeline-mcp\n\n# 2. Edit docker-compose.yml to configure environment variables\n# Example docker-compose.yml:\n# version: '3'\n# services:\n#   mcp:\n#     image: chaitin/safeline-mcp:latest\n#     container_name: safeline-mcp\n#     ports:\n#       - \"5678:5678\"\n#     environment:\n#       - SAFELINE_API_TOKEN=your_api_token\n#       - SAFELINE_ADDRESS=https://your.safeline.com\n#       - LISTEN_PORT=5678\n#       - LISTEN_ADDRESS=0.0.0.0\n\n# 3. Start service\ndocker compose -f docker-compose.yml up -d\n```\n\n#### Method 3: Using Go\n\n```bash\n# 1. Clone repository\ngit clone https://github.com/chaitin/SafeLine.git\ncd safeline-mcp\n\n# 2. Install dependencies\ngo mod download\n\n# 3. Configure config.yaml\ncp config.yaml.example config.yaml\n# Edit config.yaml with necessary configurations\n\n# 4. Run service\ngo run main.go\n```\n\nFor more API details, please refer to the [API Documentation](https://demo.waf.chaitin.com:9443/swagger/index.html).\n\n## Tools\n\n### Application Management\n\n- **create_application**\n\n### Rule Management\n- **create_blacklist_rule**\n- **create_whitelist_rule**\n\n### Analyze\n- **get_attack_events**\n\n## Development Guide\n\nThe Go API in this project is currently under development, and APIs may change. If you have specific requirements, please submit an Issue for discussion.\n\n### Directory Structure\n\n```\ninternal/\n├── api/              # API implementation\n│   ├── app/         # Application-related APIs\n│   │   └── create_application.go\n│   └── rule/        # Rule-related APIs\n│       └── create_rule.go\n└── tools/           # MCP tool implementation\n    ├── app/         # Application-related tools\n    │   └── create_application.go\n    └── rule/        # Rule-related tools\n        └── create_rule.go\n```\n\n### Adding New Tools\n\n1. **Create Tool File**\n   - Create corresponding directory and file under `internal/tools`\n   - File name should match tool name\n   - Use separate file for each tool\n   - Example: `internal/tools/app/create_application.go`\n\n2. **Tool Implementation Template**\n```go\npackage app\n\ntype ToolName struct{}\n\ntype ToolParams struct {\n    // Parameter definitions\n    Param1 string `json:\"param1\" desc:\"parameter description\" required:\"true\"`\n    Param2 int    `json:\"param2\" desc:\"parameter description\" required:\"false\"`\n}\n\ntype ToolResult struct {\n    Field1 string `json:\"field1\"`\n}\n\nfunc (t *ToolName) Name() string {\n    return \"tool_name\"\n}\n\nfunc (t *ToolName) Description() string {\n    return \"tool description\"\n}\n\nfunc (t *ToolName) Validate(params ToolParams) error {\n    // Parameter validation logic\n    return nil\n}\n\nfunc (t *ToolName) Execute(ctx context.Context, params ToolParams) (result ToolResult, err error) {\n    // Tool execution logic\n    return result, nil\n}\n```\n\n3. **[Optional]Create API Implementation**\n\nIf you need to use some APIs that have not been implemented yet, you need to create corresponding files in the api directory for implementation\n   - Create same directory structure under `internal/api`\n   - File name should match tool func\n   - Example: `internal/api/app/create_application.go`\n\n**API Implementation Template**\n```go\npackage app\n\ntype RequestType struct {\n    // Request parameter definitions\n    Param1 string `json:\"param1\"`\n    Param2 int    `json:\"param2\"`\n}\n\nfunc APIName(ctx context.Context, req *RequestType) (ResultType, error) {\n    if req == nil {\n        return nil, errors.New(\"request is required\")\n    }\n\n    var resp api.Response[ResultType]\n    err := api.Service().Post(ctx, \"/api/path\", req, &resp)\n    if err != nil {\n        return nil, errors.Wrap(err, \"failed to execute\")\n    }\n\n    if resp.Err != nil {\n        return nil, errors.New(resp.Msg)\n    }\n\n    return resp.Data, nil\n}\n```\n4. **Tool Registration (init.go)**\n\nThe tool registration file `internal/tools/init.go` is used to centrally manage all tool registrations\n  - Register all tools uniformly in the `init()` function\n  - Use the `AppendTool()` method for registration\n  - Example:\n    ```go\n    // Register create application tool\n    AppendTool(&app.CreateApp{})\n    \n    // Register create blacklist rule tool\n    AppendTool(&rule.CreateBlacklistRule{})\n    ```\n\n### Development Standards\n\n1. **Naming Conventions**\n   - Use lowercase letters and underscores for tool names\n   - File names should match tool names\n\n2. **Directory Organization**\n   - Divide directories by functional modules (e.g., app, rule, etc.)\n   - Maintain consistent structure between tools and api directories\n   - Keep related functionality in the same directory\n\n3. **Code Standards**\n   - Follow Go standard code conventions\n   - Add necessary parameter validation\n   - Use unified error handling approach\n   - Add appropriate logging\n\n4. **Documentation Requirements**\n   - Provide clear functional description in tool Description\n   - Add detailed description for parameters\n   - Update API toolkit documentation in README\n\n### Example\n\nRefer to the implementation of the `create_application` tool:\n- Tool implementation: `internal/tools/app/create_application.go`\n- API implementation: `internal/api/app/create_application.go`\n\n\n\n\n\n"
  },
  {
    "path": "mcp_server/config.yaml",
    "content": "# Server Configuration\nserver:\n  name: \"SafeLine MCP Server\"\n  version: \"1.0.0\"\n  # Can be overridden by environment variable LISTEN_PORT\n  port: 5678\n  # Can be overridden by environment variable LISTEN_ADDRESS\n  host: \"0.0.0.0\"\n  # Can be overridden by environment variable SAFELINE_SECRET\n  secret: \"\" # Secret for SSE server\n# Logger Configuration\nlogger:\n  level: \"info\" # Log level: debug, info, warn, error\n  file_path: \"\" # Log file path\n  console: true # Whether to output to console\n  caller: false # Whether to record caller information\n  development: true # Whether to use development mode\n\n# API Configuration\napi:\n  # Can be overridden by environment variable SAFELINE_ADDRESS\n  base_url: \"\" # API service address\n  # Can be overridden by environment variable SAFELINE_API_TOKEN\n  token: \"\" # Authentication token\n  timeout: 30 # Timeout in seconds\n  debug: false # Whether to enable debug mode\n  insecure_skip_verify: true # Whether to skip certificate verification\n"
  },
  {
    "path": "mcp_server/docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  mcp_server:\n    image: chaitin/safeline-mcp:latest\n    container_name: mcp_server\n    restart: always\n    ports:\n      - \"5678:5678\"\n    environment:\n      - SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate\n      - SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address\n      - SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token  \n      - LISTEN_PORT=5678 # optional, default is 5678\n      - LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0 "
  },
  {
    "path": "mcp_server/go.mod",
    "content": "module github.com/chaitin/SafeLine/mcp_server\n\ngo 1.24.1\n\nrequire (\n\tgithub.com/mark3labs/mcp-go v0.18.0\n\tgo.uber.org/zap v1.27.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/mcuadros/go-defaults v1.2.0\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n)\n"
  },
  {
    "path": "mcp_server/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=\ngithub.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=\ngithub.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=\ngithub.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "mcp_server/internal/api/analyze/get_event_list.go",
    "content": "package analyze\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n)\n\ntype GetEventListRequest struct {\n\tPage     int    `json:\"page\"`\n\tPageSize int    `json:\"page_size\"`\n\tIP       string `json:\"ip\"`\n\tStart    int64  `json:\"start\"`\n\tEnd      int64  `json:\"end\"`\n}\n\ntype GetEventListResponse struct {\n\tNodes []Event `json:\"nodes\"`\n\tTotal int64   `json:\"total\"`\n}\n\ntype Event struct {\n\tID        uint   `json:\"id\"`\n\tIP        string `json:\"ip\"`\n\tProtocol  int    `json:\"protocol\"`\n\tHost      string `json:\"host\"`\n\tDstPort   uint64 `json:\"dst_port\"`\n\tUpdatedAt int64  `json:\"updated_at\"`\n\tStartAt   int64  `json:\"start_at\"`\n\tEndAt     int64  `json:\"end_at\"`\n\tDenyCount int64  `json:\"deny_count\"`\n\tPassCount int64  `json:\"pass_count\"`\n\tFinished  bool   `json:\"finished\"`\n\tCountry   string `json:\"country\"`\n\tProvince  string `json:\"province\"`\n\tCity      string `json:\"city\"`\n}\n\nfunc GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) {\n\tvar resp api.Response[GetEventListResponse]\n\terr := api.Service().Get(ctx, fmt.Sprintf(\"/api/open/events?page=%d&page_size=%d&ip=%s&start=%d&end=%d\", req.Page, req.PageSize, req.IP, req.Start, req.End), &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.Data, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/api/app/create_application.go",
    "content": "package app\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n)\n\ntype CreateAppRequest struct {\n\tServerNames []string `json:\"server_names\"`\n\tPorts       []string `json:\"ports\"`\n\tUpstreams   []string `json:\"upstreams\"`\n\tComment     string   `json:\"comment\"`\n}\n\n// CreateApp Create new website or app\nfunc CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) {\n\tif req == nil {\n\t\treturn 0, errors.New(\"request is required\")\n\t}\n\n\tvar resp api.Response[int64]\n\terr := api.Service().Post(ctx, \"/api/open/site\", req, &resp)\n\tif err != nil {\n\t\treturn 0, errors.Wrap(err, \"failed to create app\")\n\t}\n\n\tif resp.Err != nil {\n\t\treturn 0, errors.New(resp.Msg)\n\t}\n\n\treturn resp.Data, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/api/client.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\n// Client API client\ntype Client struct {\n\tbaseURL    string\n\thttpClient *http.Client\n\theaders    map[string]string\n}\n\n// ClientOption Client configuration options\ntype ClientOption func(*Client)\n\n// WithTimeout Set timeout duration\nfunc WithTimeout(timeout time.Duration) ClientOption {\n\treturn func(c *Client) {\n\t\tc.httpClient.Timeout = timeout\n\t}\n}\n\n// WithHeader Set request header\nfunc WithHeader(key, value string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.headers[key] = value\n\t}\n}\n\n// WithBaseURL Set base URL\nfunc WithBaseURL(baseURL string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.baseURL = baseURL\n\t}\n}\n\n// WithInsecureSkipVerify Set whether to skip certificate verification\nfunc WithInsecureSkipVerify(skip bool) ClientOption {\n\treturn func(c *Client) {\n\t\tif transport, ok := c.httpClient.Transport.(*http.Transport); ok {\n\t\t\tif transport.TLSClientConfig == nil {\n\t\t\t\ttransport.TLSClientConfig = &tls.Config{}\n\t\t\t}\n\t\t\ttransport.TLSClientConfig.InsecureSkipVerify = skip\n\t\t}\n\t}\n}\n\n// NewClient Create new API client\nfunc NewClient(opts ...ClientOption) *Client {\n\ttransport := &http.Transport{\n\t\tTLSClientConfig: &tls.Config{},\n\t}\n\n\tc := &Client{\n\t\thttpClient: &http.Client{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tTransport: transport,\n\t\t},\n\t\theaders: make(map[string]string),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\n\treturn c\n}\n\n// Request Send request\nfunc (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {\n\treqURL := fmt.Sprintf(\"%s%s\", c.baseURL, path)\n\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"marshal request body failed\")\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\tlogger.With(\"url\", reqURL).Debug(\"request url\")\n\treq, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"create request failed\")\n\t}\n\n\t// Set common headers\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tfor k, v := range c.headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"send request failed\")\n\t}\n\tdefer resp.Body.Close()\n\n\t// Read response body\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"read response body failed\")\n\t}\n\n\t// Check status code\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn errors.New(fmt.Sprintf(\"request failed with status %d: %s\", resp.StatusCode, string(respBody)))\n\t}\n\n\t// Parse response\n\tif result != nil {\n\t\tif err := json.Unmarshal(respBody, result); err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tvar respData map[string]interface{}\n\t\tif err := json.Unmarshal(respBody, &respData); err != nil {\n\t\t\treturn errors.Wrap(err, \"unmarshal response failed\")\n\t\t}\n\t\tif respData[\"err\"] != nil || respData[\"msg\"] != nil {\n\t\t\treturn errors.New(respData[\"msg\"].(string))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Get Send GET request\nfunc (c *Client) Get(ctx context.Context, path string, result interface{}) error {\n\treturn c.Request(ctx, http.MethodGet, path, nil, result)\n}\n\n// Post Send POST request\nfunc (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {\n\treturn c.Request(ctx, http.MethodPost, path, body, result)\n}\n\n// Put Send PUT request\nfunc (c *Client) Put(ctx context.Context, path string, body interface{}, result interface{}) error {\n\treturn c.Request(ctx, http.MethodPut, path, body, result)\n}\n\n// Delete Send DELETE request\nfunc (c *Client) Delete(ctx context.Context, path string, result interface{}) error {\n\treturn c.Request(ctx, http.MethodDelete, path, nil, result)\n}\n"
  },
  {
    "path": "mcp_server/internal/api/response.go",
    "content": "package api\n\n// Response Common API response structure\ntype Response[T any] struct {\n\t// Response data\n\tData T `json:\"data\"`\n\t// Error message\n\tErr any `json:\"err\"`\n\t// Prompt message\n\tMsg string `json:\"msg\"`\n}\n"
  },
  {
    "path": "mcp_server/internal/api/rule/create_rule.go",
    "content": "package rule\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n)\n\ntype CreateRuleRequest struct {\n\tName      string          `json:\"name\"`\n\tIP        []string        `json:\"ip\"`\n\tIsEnabled bool            `json:\"is_enabled\"`\n\tPattern   [][]api.Pattern `json:\"pattern\"`\n\tAction    int             `json:\"action\"`\n}\n\n// CreateRule Create new rule\nfunc CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) {\n\tif req == nil {\n\t\treturn 0, errors.New(\"request is required\")\n\t}\n\n\tvar resp api.Response[int64]\n\terr := api.Service().Post(ctx, \"/api/open/policy\", req, &resp)\n\tif err != nil {\n\t\treturn 0, errors.Wrap(err, \"failed to create policy rule\")\n\t}\n\n\tif resp.Err != nil {\n\t\treturn 0, errors.New(resp.Msg)\n\t}\n\n\treturn resp.Data, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/api/service.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/config\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\n// APIClient API client implementation\ntype APIClient struct {\n\tclient *Client\n\tconfig *config.APIConfig\n}\n\nvar (\n\tinstance *APIClient\n\tonce     sync.Once\n)\n\n// Init Initialize API service\nfunc Init(cfg *config.APIConfig) error {\n\tvar err error\n\tonce.Do(func() {\n\t\tinstance, err = newAPIClient(cfg)\n\t\tif err != nil {\n\t\t\tlogger.With(\"error\", err).Error(\"failed to initialize API service\")\n\t\t\treturn\n\t\t}\n\t\tlogger.Info(\"API service initialized successfully\")\n\t})\n\treturn err\n}\n\n// Service Get API service instance\nfunc Service() *APIClient {\n\tif instance == nil {\n\t\tlogger.Error(\"API service not initialized\")\n\t\tpanic(\"API service not initialized\")\n\t}\n\treturn instance\n}\n\n// newAPIClient Create new API client\nfunc newAPIClient(config *config.APIConfig) (*APIClient, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"config is required\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\treturn nil, errors.New(\"base_url is required\")\n\t}\n\n\ttimeout := 30\n\tif config.Timeout > 0 {\n\t\ttimeout = config.Timeout\n\t}\n\n\topts := []ClientOption{\n\t\tWithBaseURL(config.BaseURL),\n\t\tWithTimeout(time.Duration(timeout) * time.Second),\n\t\tWithHeader(\"User-Agent\", \"SafeLine-MCP/1.0\"),\n\t\tWithInsecureSkipVerify(config.InsecureSkipVerify),\n\t}\n\n\t// If token is configured, add authentication header\n\tif config.Token != \"\" {\n\t\topts = append(opts, WithHeader(\"X-SLCE-API-TOKEN\", config.Token))\n\t}\n\n\tclient := NewClient(opts...)\n\n\treturn &APIClient{\n\t\tclient: client,\n\t\tconfig: config,\n\t}, nil\n}\n\n// Post Send POST request\nfunc (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error {\n\treturn c.client.Request(ctx, \"POST\", path, body, result)\n}\n\n// Get Send GET request\nfunc (c *APIClient) Get(ctx context.Context, path string, result interface{}) error {\n\treturn c.client.Request(ctx, \"GET\", path, nil, result)\n}\n\n// Put Send PUT request\nfunc (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error {\n\treturn c.client.Request(ctx, \"PUT\", path, body, result)\n}\n\n// Delete Send DELETE request\nfunc (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error {\n\treturn c.client.Request(ctx, \"DELETE\", path, nil, result)\n}\n"
  },
  {
    "path": "mcp_server/internal/api/types.go",
    "content": "package api\n\ntype PolicyRuleAction int\n\nconst (\n\tPolicyRuleActionAllow PolicyRuleAction = iota\n\tPolicyRuleActionDeny\n\tPolicyRuleActionMax\n)\n\ntype Key = string\n\nconst (\n\tKeySrcIP      Key = \"src_ip\"\n\tKeyURI        Key = \"uri\"\n\tKeyURINoQuery Key = \"uri_no_query\"\n\tKeyHost       Key = \"host\"\n\tKeyMethod     Key = \"method\"\n\tKeyReqHeader  Key = \"req_header\"\n\tKeyReqBody    Key = \"req_body\"\n\tKeyGetParam   Key = \"get_param\"\n\tKeyPostParam  Key = \"post_param\"\n)\n\ntype Op = string\n\nconst (\n\tOpEq       Op = \"eq\"         // equal\n\tOpNotEq    Op = \"not_eq\"     // not equal\n\tOpMatch    Op = \"match\"      // match\n\tOpCIDR     Op = \"cidr\"       // cidr\n\tOpHas      Op = \"has\"        // has\n\tOpNotHas   Op = \"not_has\"    // not has\n\tOpPrefix   Op = \"prefix\"     // prefix\n\tOpRe       Op = \"re\"         // regex\n\tOpIn       Op = \"in\"         // in\n\tOpNotIn    Op = \"not_in\"     // not in\n\tOpNotCIDR  Op = \"not_cidr\"   // not cidr\n\tOpExist    Op = \"exist\"      // exist\n\tOpNotExist Op = \"not_exist\"  // not exist\n\tOpGeoEq    Op = \"geo_eq\"     // geo equal\n\tOpGeoNotEq Op = \"geo_not_eq\" // geo not equal\n)\n\ntype Pattern struct {\n\tK    Key      `json:\"k\"`\n\tOp   Op       `json:\"op\"`\n\tV    []string `json:\"v\"`\n\tSubK string   `json:\"sub_k\"`\n}\n"
  },
  {
    "path": "mcp_server/internal/config/config.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Config Global configuration structure\ntype Config struct {\n\tServer *ServerConfig `yaml:\"server\"`\n\tLogger *LoggerConfig `yaml:\"logger\"`\n\tAPI    *APIConfig    `yaml:\"api\"`\n}\n\n// APIConfig API configuration\ntype APIConfig struct {\n\t// API base URL\n\tBaseURL string `yaml:\"base_url\"`\n\t// API token\n\tToken string `yaml:\"token\"`\n\t// API timeout\n\tTimeout int `yaml:\"timeout\"`\n\t// API debug mode\n\tDebug bool `yaml:\"debug\"`\n\t// API insecure skip verify\n\tInsecureSkipVerify bool `yaml:\"insecure_skip_verify\"`\n}\n\n// ServerConfig Server configuration\ntype ServerConfig struct {\n\tName    string `yaml:\"name\"`\n\tVersion string `yaml:\"version\"`\n\tPort    int    `yaml:\"port\"`\n\tHost    string `yaml:\"host\"`\n\tSecret  string `yaml:\"secret\"`\n}\n\n// LoggerConfig Logger configuration\ntype LoggerConfig struct {\n\tLevel       string `yaml:\"level\"`\n\tFilePath    string `yaml:\"file_path\"`\n\tConsole     bool   `yaml:\"console\"`\n\tCaller      bool   `yaml:\"caller\"`\n\tDevelopment bool   `yaml:\"development\"`\n}\n\nvar config *Config\n\n// getEnvString Get string value from environment variable, return default value if not exists\nfunc getEnvString(key, defaultValue string) string {\n\tif value, exists := os.LookupEnv(key); exists {\n\t\treturn value\n\t}\n\treturn defaultValue\n}\n\n// getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed\nfunc getEnvInt(key string, defaultValue int) int {\n\tif value, exists := os.LookupEnv(key); exists {\n\t\tif intValue, err := strconv.Atoi(value); err == nil {\n\t\t\treturn intValue\n\t\t}\n\t}\n\treturn defaultValue\n}\n\n// Load Load configuration file\nfunc Load(path string) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"read config file failed\")\n\t}\n\n\tconfig = &Config{}\n\tif err := yaml.Unmarshal(data, config); err != nil {\n\t\treturn errors.Wrap(err, \"unmarshal config failed\")\n\t}\n\n\t// Override configuration from environment variables\n\tif config.Server != nil {\n\t\tconfig.Server.Host = getEnvString(\"LISTEN_ADDRESS\", config.Server.Host)\n\t\tconfig.Server.Port = getEnvInt(\"LISTEN_PORT\", config.Server.Port)\n\t}\n\n\tif config.API != nil {\n\t\tconfig.API.BaseURL = getEnvString(\"SAFELINE_ADDRESS\", config.API.BaseURL)\n\t\tconfig.API.Token = getEnvString(\"SAFELINE_API_TOKEN\", config.API.Token)\n\t}\n\n\treturn nil\n}\n\n// GetServer Get server configuration\nfunc GetServer() *ServerConfig {\n\tif config == nil {\n\t\treturn nil\n\t}\n\treturn config.Server\n}\n\n// GetLogger Get logger configuration\nfunc GetLogger() *LoggerConfig {\n\tif config == nil {\n\t\treturn nil\n\t}\n\treturn config.Logger\n}\n\n// GetAPI Get API configuration\nfunc GetAPI() *APIConfig {\n\tif config == nil {\n\t\treturn nil\n\t}\n\treturn config.API\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/analyze/get_atttack_events.go",
    "content": "package analyze\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api/analyze\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\ntype GetAttackEventsParams struct {\n\tIP       string `json:\"ip\" desc:\"ip\" required:\"false\"`\n\tPage     int    `json:\"page\" desc:\"page\" required:\"false\" default:\"1\"`\n\tPageSize int    `json:\"page_size\" desc:\"page size\" required:\"false\" default:\"10\"`\n\tStart    int64  `json:\"start\" desc:\"start unix timestamp in milliseconds\" required:\"false\"`\n\tEnd      int64  `json:\"end\" desc:\"end unix timestamp in milliseconds\" required:\"false\"`\n}\n\ntype GetAttackEvents struct{}\n\nfunc (t *GetAttackEvents) Name() string {\n\treturn \"get_attack_events\"\n}\n\nfunc (t *GetAttackEvents) Description() string {\n\treturn \"get attack events\"\n}\n\nfunc (t *GetAttackEvents) Validate(params GetAttackEventsParams) error {\n\treturn nil\n}\n\nfunc (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) {\n\tresp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{\n\t\tIP:       params.IP,\n\t\tPageSize: params.PageSize,\n\t\tPage:     params.Page,\n\t\tStart:    params.Start,\n\t\tEnd:      params.End,\n\t})\n\tif err != nil {\n\t\treturn analyze.GetEventListResponse{}, err\n\t}\n\tlogger.With(\"total\", resp.Total).Info(\"get attack events\")\n\treturn *resp, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/app/create_application.go",
    "content": "package app\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api/app\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\ntype CreateApp struct{}\n\ntype CreateAppParams struct {\n\tServerNames []string `json:\"server_names\" desc:\"domain list\" required:\"true\"`\n\tPorts       []string `json:\"ports\" desc:\"port list\" required:\"true\"`\n\tUpstreams   []string `json:\"upstreams\" desc:\"upstream list\" required:\"true\"`\n}\n\nfunc (t *CreateApp) Name() string {\n\treturn \"create_http_application\"\n}\n\nfunc (t *CreateApp) Description() string {\n\treturn \"create a new website or app\"\n}\n\nfunc (t *CreateApp) Validate(params CreateAppParams) error {\n\treturn nil\n}\n\nfunc (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) {\n\tid, err := app.CreateApp(ctx, &app.CreateAppRequest{\n\t\tServerNames: params.ServerNames,\n\t\tPorts:       params.Ports,\n\t\tUpstreams:   params.Upstreams,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tlogger.Info(\"create app success\", logger.Int64(\"id\", id))\n\treturn id, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/example.go",
    "content": "package tools\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\ntype CalculateSum struct{}\n\nfunc (t *CalculateSum) Name() string {\n\treturn \"calculate_sum\"\n}\n\nfunc (t *CalculateSum) Description() string {\n\treturn \"Add two numbers together\"\n}\n\ntype MyToolInput struct {\n\tA int `json:\"a\" desc:\"number a\" required:\"true\"`\n\tB int `json:\"b\" desc:\"number b\" required:\"true\"`\n}\n\ntype MyToolOutput struct {\n\tC int `json:\"c\"`\n}\n\nfunc (t *CalculateSum) Validate(params MyToolInput) error {\n\treturn nil\n}\n\nfunc (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) {\n\tlogger.With(\"a\", params.A).\n\t\tWith(\"b\", params.B).\n\t\tDebug(\"Executing calculation\")\n\n\tresult := MyToolOutput{\n\t\tC: params.A + params.B,\n\t}\n\n\tlogger.With(\"result\", result.C).\n\t\tDebug(\"Calculation completed\")\n\n\treturn result, errors.New(\"test error\")\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/init.go",
    "content": "package tools\n\nimport (\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/tools/app\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/tools/rule\"\n)\n\nfunc init() {\n\t// app\n\tAppendTool(&app.CreateApp{})\n\n\t// rule\n\tAppendTool(&rule.CreateBlacklistRule{})\n\tAppendTool(&rule.CreateWhitelistRule{})\n\n\t// analyze\n\tAppendTool(&analyze.GetAttackEvents{})\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/rule/create_blacklist_rule.go",
    "content": "package rule\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api/rule\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\ntype CreateBlacklistRule struct{}\n\ntype CreateBlacklistRuleParams struct {\n\tName       string   `json:\"name\" desc:\"name\" required:\"true\"`\n\tIP         []string `json:\"ip\" desc:\"ip\" required:\"false\"`\n\tURINoQuery []string `json:\"uri_no_query\" desc:\"uri_no_query\" required:\"false\"`\n}\n\nfunc (t *CreateBlacklistRule) Name() string {\n\treturn \"create_blacklist_rule\"\n}\n\nfunc (t *CreateBlacklistRule) Description() string {\n\treturn \"create a new blacklist rule\"\n}\n\nfunc (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error {\n\treturn nil\n}\n\nfunc (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) {\n\tvar pattern [][]api.Pattern\n\tif len(params.IP) > 0 {\n\t\tpattern = append(pattern, []api.Pattern{\n\t\t\t{\n\t\t\t\tK:    api.KeySrcIP,\n\t\t\t\tOp:   api.OpEq,\n\t\t\t\tV:    params.IP,\n\t\t\t\tSubK: \"\",\n\t\t\t},\n\t\t})\n\t}\n\tif len(params.URINoQuery) > 0 {\n\t\tpattern = append(pattern, []api.Pattern{\n\t\t\t{\n\t\t\t\tK:    api.KeyURINoQuery,\n\t\t\t\tOp:   api.OpEq,\n\t\t\t\tV:    params.URINoQuery,\n\t\t\t\tSubK: \"\",\n\t\t\t},\n\t\t})\n\t}\n\n\tid, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{\n\t\tName:      params.Name,\n\t\tIP:        params.IP,\n\t\tIsEnabled: true,\n\t\tAction:    int(api.PolicyRuleActionDeny),\n\t\tPattern:   pattern,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tlogger.With(\"id\", id).Info(\"create blacklist rule success\")\n\treturn id, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/rule/create_whitelist_rule.go",
    "content": "package rule\n\nimport (\n\t\"context\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api/rule\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\ntype CreateWhitelistRule struct{}\n\ntype CreateWhitelistRuleParams struct {\n\tName       string   `json:\"name\" desc:\"name\" required:\"true\"`\n\tIP         []string `json:\"ip\" desc:\"ip\" required:\"false\"`\n\tURINoQuery []string `json:\"uri_no_query\" desc:\"uri_no_query\" required:\"false\"`\n}\n\nfunc (t *CreateWhitelistRule) Name() string {\n\treturn \"create_whitelist_rule\"\n}\n\nfunc (t *CreateWhitelistRule) Description() string {\n\treturn \"create a new whitelist rule\"\n}\n\nfunc (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error {\n\treturn nil\n}\n\nfunc (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) {\n\tvar pattern [][]api.Pattern\n\tif len(params.IP) > 0 {\n\t\tpattern = append(pattern, []api.Pattern{\n\t\t\t{\n\t\t\t\tK:    api.KeySrcIP,\n\t\t\t\tOp:   api.OpEq,\n\t\t\t\tV:    params.IP,\n\t\t\t\tSubK: \"\",\n\t\t\t},\n\t\t})\n\t}\n\tif len(params.URINoQuery) > 0 {\n\t\tpattern = append(pattern, []api.Pattern{\n\t\t\t{\n\t\t\t\tK:    api.KeyURINoQuery,\n\t\t\t\tOp:   api.OpEq,\n\t\t\t\tV:    params.URINoQuery,\n\t\t\t\tSubK: \"\",\n\t\t\t},\n\t\t})\n\t}\n\n\tid, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{\n\t\tName:      params.Name,\n\t\tIP:        params.IP,\n\t\tIsEnabled: true,\n\t\tAction:    int(api.PolicyRuleActionAllow),\n\t\tPattern:   pattern,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tlogger.With(\"id\", id).Info(\"create whitelist rule success\")\n\treturn id, nil\n}\n"
  },
  {
    "path": "mcp_server/internal/tools/tool.go",
    "content": "package tools\n\nimport (\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/mcp\"\n)\n\n// By deferring the concretization of generic types to the Register method,\n// we avoid type inference issues.\n\n// Each Tool is wrapped in a toolWrapper that knows its concrete type,\n// allowing correct passing of generic parameters during registration.\ntype ToolWrapper interface {\n\tRegister(s *mcp.MCPServer) error\n}\n\nvar (\n\ttools = []ToolWrapper{}\n)\n\nfunc AppendTool[T any, R any](tool ...mcp.Tool[T, R]) {\n\tfor _, t := range tool {\n\t\ttools = append(tools, &toolWrapper[T, R]{tool: t})\n\t}\n}\n\nfunc Tools() []ToolWrapper {\n\treturn tools\n}\n\ntype toolWrapper[T any, R any] struct {\n\ttool mcp.Tool[T, R]\n}\n\nfunc (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error {\n\tlogger.Info(\"Registering tool\",\n\t\tlogger.String(\"name\", w.tool.Name()),\n\t\tlogger.String(\"description\", w.tool.Description()),\n\t)\n\treturn mcp.RegisterTool(s, w.tool)\n}\n"
  },
  {
    "path": "mcp_server/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/api\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/config\"\n\t\"github.com/chaitin/SafeLine/mcp_server/internal/tools\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/mcp\"\n)\n\nfunc main() {\n\tconfigPath := flag.String(\"config\", \"config.yaml\", \"path to config file\")\n\tflag.Parse()\n\n\tif err := config.Load(*configPath); err != nil {\n\t\tpanic(fmt.Errorf(\"failed to load config: %v\", err))\n\t}\n\n\tlogConfig := config.GetLogger()\n\tif err := logger.Init(&logger.Config{\n\t\tLevel:       logConfig.Level,\n\t\tFilePath:    logConfig.FilePath,\n\t\tConsole:     logConfig.Console,\n\t\tCaller:      logConfig.Caller,\n\t\tDevelopment: logConfig.Development,\n\t}); err != nil {\n\t\tpanic(fmt.Errorf(\"failed to init logger: %v\", err))\n\t}\n\n\tlogger.With(\"base_url\", config.GetAPI().BaseURL).Info(\"Initializing API service...\")\n\tif err := api.Init(config.GetAPI()); err != nil {\n\t\tpanic(fmt.Errorf(\"failed to init API service: %v\", err))\n\t}\n\n\tlogger.Info(\"Starting MCP Server...\")\n\tserverConfig := config.GetServer()\n\ts := mcp.NewMCPServer(\n\t\tserverConfig.Name,\n\t\tserverConfig.Version,\n\t\tserverConfig.Secret,\n\t)\n\n\tlogger.Info(\"Registering tools...\")\n\tfor _, tool := range tools.Tools() {\n\t\tif err := tool.Register(s); err != nil {\n\t\t\tlogger.With(\"error\", err).\n\t\t\t\tError(\"Failed to register tool\")\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\taddr := fmt.Sprintf(\"%s:%d\", serverConfig.Host, serverConfig.Port)\n\tlogger.With(\"addr\", addr).Info(\"Starting server\")\n\tif err := s.Start(addr); err != nil {\n\t\tlogger.With(\"error\", err).\n\t\t\tError(\"Server failed to start\")\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "mcp_server/pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Config Global configuration structure\ntype Config struct {\n\tServer ServerConfig `yaml:\"server\"`\n\tLogger LoggerConfig `yaml:\"logger\"`\n}\n\n// ServerConfig Server configuration\ntype ServerConfig struct {\n\tName    string `yaml:\"name\"`\n\tVersion string `yaml:\"version\"`\n\tPort    int    `yaml:\"port\"`\n\tHost    string `yaml:\"host\"`\n\tSecret  string `yaml:\"secret\"`\n}\n\n// LoggerConfig Logger configuration\ntype LoggerConfig struct {\n\tLevel       string `yaml:\"level\"`\n\tFilePath    string `yaml:\"file_path\"`\n\tConsole     bool   `yaml:\"console\"`\n\tCaller      bool   `yaml:\"caller\"`\n\tDevelopment bool   `yaml:\"development\"`\n}\n\nvar (\n\tglobalConfig *Config\n)\n\n// Load Load configuration from file\nfunc Load(filename string) error {\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfig := &Config{}\n\tif err := yaml.Unmarshal(data, config); err != nil {\n\t\treturn err\n\t}\n\n\tglobalConfig = config\n\treturn nil\n}\n\n// Get Get global configuration\nfunc Get() *Config {\n\treturn globalConfig\n}\n\n// GetServer Get server configuration\nfunc GetServer() ServerConfig {\n\tif globalConfig == nil {\n\t\treturn ServerConfig{}\n\t}\n\treturn globalConfig.Server\n}\n\n// GetLogger Get logger configuration\nfunc GetLogger() LoggerConfig {\n\tif globalConfig == nil {\n\t\treturn LoggerConfig{}\n\t}\n\treturn globalConfig.Logger\n}\n"
  },
  {
    "path": "mcp_server/pkg/errors/errors.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n)\n\nvar (\n\t// Common errors\n\tErrInternal     = New(\"internal error\")\n\tErrInvalidParam = New(\"invalid parameter\")\n\tErrNotFound     = New(\"resource not found\")\n\tErrUnauthorized = New(\"unauthorized\")\n\tErrForbidden    = New(\"forbidden\")\n\tErrTimeout      = New(\"timeout\")\n)\n\n// Error Custom error structure\ntype Error struct {\n\terr      error\n\tstack    []string\n\tmsg      string\n\tlocation string\n}\n\n// Error Implement error interface\nfunc (e *Error) Error() string {\n\tif e.msg != \"\" {\n\t\treturn fmt.Sprintf(\"%s: %v (at %s)\", e.msg, e.err, e.location)\n\t}\n\treturn fmt.Sprintf(\"%v (at %s)\", e.err, e.location)\n}\n\n// Unwrap Return original error\nfunc (e *Error) Unwrap() error {\n\tif e.err == nil {\n\t\treturn nil\n\t}\n\tif wrapped, ok := e.err.(*Error); ok {\n\t\treturn wrapped.Unwrap()\n\t}\n\treturn e.err\n}\n\n// Stack Return error stack\nfunc (e *Error) Stack() []string {\n\treturn e.stack\n}\n\n// Location Return error location\nfunc (e *Error) Location() string {\n\treturn e.location\n}\n\n// getCallerLocation Get caller location\nfunc getCallerLocation(skip int) string {\n\t_, file, line, ok := runtime.Caller(skip)\n\tif !ok {\n\t\treturn \"unknown\"\n\t}\n\treturn fmt.Sprintf(\"%s:%d\", file, line)\n}\n\n// WrapL Wrap error and print log\nfunc WrapL(err error, msg string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// Get stack trace information\n\tvar stack []string\n\tfor i := 1; i < 32; i++ {\n\t\tpc, file, line, ok := runtime.Caller(i)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tfn := runtime.FuncForPC(pc)\n\t\tif fn == nil {\n\t\t\tbreak\n\t\t}\n\t\tname := fn.Name()\n\t\tif strings.Contains(name, \"runtime.\") {\n\t\t\tbreak\n\t\t}\n\t\tstack = append(stack, fmt.Sprintf(\"%s:%d\", file, line))\n\t}\n\n\twrappedErr := &Error{\n\t\terr:      err,\n\t\tstack:    stack,\n\t\tmsg:      msg,\n\t\tlocation: getCallerLocation(2),\n\t}\n\n\t// Print error information and stack using logger\n\tlogger.With(\"error\", err).\n\t\tWith(\"location\", wrappedErr.location).\n\t\tWith(\"stack\", strings.Join(stack, \"\\n\")).\n\t\tError(msg)\n\n\treturn wrappedErr\n}\n\n// Is Check error type\nfunc Is(err, target error) bool {\n\treturn errors.Is(err, target)\n}\n\n// As Type assertion\nfunc As(err error, target interface{}) bool {\n\treturn errors.As(err, target)\n}\n\n// Wrap Wrap error without printing log\nfunc Wrap(err error, msg string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\treturn &Error{\n\t\terr:      err,\n\t\tmsg:      msg,\n\t\tlocation: getCallerLocation(2),\n\t}\n}\n\n// New Create new error\nfunc New(text string) error {\n\treturn &Error{\n\t\terr:      errors.New(text),\n\t\tlocation: getCallerLocation(2),\n\t}\n}\n"
  },
  {
    "path": "mcp_server/pkg/logger/field.go",
    "content": "package logger\n\nimport (\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// Field 日志字段\ntype Field = zapcore.Field\n\n// String 创建字符串字段\nfunc String(key string, val string) Field {\n\treturn zap.String(key, val)\n}\n\n// Int 创建整数字段\nfunc Int(key string, val int) Field {\n\treturn zap.Int(key, val)\n}\n\n// Int64 创建 int64 字段\nfunc Int64(key string, val int64) Field {\n\treturn zap.Int64(key, val)\n}\n\n// Float64 创建浮点数字段\nfunc Float64(key string, val float64) Field {\n\treturn zap.Float64(key, val)\n}\n\n// Bool 创建布尔字段\nfunc Bool(key string, val bool) Field {\n\treturn zap.Bool(key, val)\n}\n\n// Err 创建错误字段\nfunc Err(err error) Field {\n\treturn zap.Error(err)\n}\n\n// Any 创建任意类型字段\nfunc Any(key string, val interface{}) Field {\n\treturn zap.Any(key, val)\n}\n\n// Duration 创建时间段字段\nfunc Duration(key string, val float64) Field {\n\treturn zap.Float64(key, val)\n}\n"
  },
  {
    "path": "mcp_server/pkg/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// Logger 封装 zap.Logger\ntype Logger struct {\n\tzl *zap.Logger\n}\n\nvar (\n\tdefaultLogger *Logger\n\tonce          sync.Once\n)\n\n// Config 日志配置\ntype Config struct {\n\t// 日志级别\n\tLevel string `json:\"level\" yaml:\"level\"`\n\t// 日志文件路径\n\tFilePath string `json:\"file_path\" yaml:\"file_path\"`\n\t// 是否输出到控制台\n\tConsole bool `json:\"console\" yaml:\"console\"`\n\t// 是否记录调用者信息\n\tCaller bool `json:\"caller\" yaml:\"caller\"`\n\t// 是否使用开发模式（更详细的日志）\n\tDevelopment bool `json:\"development\" yaml:\"development\"`\n}\n\n// 默认配置\nvar defaultConfig = Config{\n\tLevel:       \"info\",\n\tFilePath:    \"logs/mcp.log\",\n\tConsole:     true,\n\tCaller:      true,\n\tDevelopment: false,\n}\n\n// Init 初始化日志系统\nfunc Init(cfg *Config) error {\n\tvar err error\n\tonce.Do(func() {\n\t\tif cfg == nil {\n\t\t\tcfg = &defaultConfig\n\t\t}\n\n\t\t// 确保日志目录存在\n\t\tif cfg.FilePath != \"\" {\n\t\t\tdir := filepath.Dir(cfg.FilePath)\n\t\t\tif err = os.MkdirAll(dir, 0755); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// 配置编码器\n\t\tencoderConfig := zapcore.EncoderConfig{\n\t\t\tTimeKey:       \"time\",\n\t\t\tLevelKey:      \"level\",\n\t\t\tNameKey:       \"logger\",\n\t\t\tCallerKey:     \"caller\",\n\t\t\tMessageKey:    \"msg\",\n\t\t\tStacktraceKey: \"stacktrace\",\n\t\t\tLineEnding:    zapcore.DefaultLineEnding,\n\t\t\tEncodeLevel:   zapcore.LowercaseLevelEncoder,\n\t\t\tEncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {\n\t\t\t\tenc.AppendString(t.Format(\"2006-01-02 15:04:05.000\"))\n\t\t\t},\n\t\t\tEncodeDuration: zapcore.SecondsDurationEncoder,\n\t\t\tEncodeCaller:   zapcore.ShortCallerEncoder,\n\t\t}\n\n\t\t// 设置日志级别\n\t\tvar level zapcore.Level\n\t\tif err = level.UnmarshalText([]byte(cfg.Level)); err != nil {\n\t\t\tlevel = zapcore.InfoLevel\n\t\t}\n\n\t\t// 创建Core\n\t\tvar cores []zapcore.Core\n\n\t\t// 文件输出\n\t\tif cfg.FilePath != \"\" {\n\t\t\tfileWriter, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcores = append(cores, zapcore.NewCore(\n\t\t\t\tzapcore.NewJSONEncoder(encoderConfig),\n\t\t\t\tzapcore.AddSync(fileWriter),\n\t\t\t\tlevel,\n\t\t\t))\n\t\t}\n\n\t\t// 控制台输出\n\t\tif cfg.Console {\n\t\t\tcores = append(cores, zapcore.NewCore(\n\t\t\t\tzapcore.NewConsoleEncoder(encoderConfig),\n\t\t\t\tzapcore.AddSync(os.Stdout),\n\t\t\t\tlevel,\n\t\t\t))\n\t\t}\n\n\t\t// 创建Logger\n\t\tcore := zapcore.NewTee(cores...)\n\t\tzl := zap.New(core)\n\n\t\t// 是否记录调用者信息\n\t\tif cfg.Caller {\n\t\t\tzl = zl.WithOptions(zap.AddCaller())\n\t\t}\n\n\t\t// 是否使用开发模式\n\t\tif cfg.Development {\n\t\t\tzl = zl.WithOptions(zap.Development())\n\t\t}\n\n\t\tdefaultLogger = &Logger{zl: zl}\n\t})\n\n\treturn err\n}\n\n// GetLogger 获取日志实例\nfunc GetLogger() *Logger {\n\tif defaultLogger == nil {\n\t\tInit(nil)\n\t}\n\treturn defaultLogger\n}\n\n// With 创建带有字段的新Logger\nfunc (l *Logger) With(key string, value interface{}) *Logger {\n\treturn &Logger{zl: l.zl.With(Any(key, value))}\n}\n\n// Debug level\nfunc (l *Logger) Debug(msg string, fields ...Field) {\n\tl.zl.Debug(msg, fields...)\n}\n\n// Info level\nfunc (l *Logger) Info(msg string, fields ...Field) {\n\tl.zl.Info(msg, fields...)\n}\n\n// Warn level\nfunc (l *Logger) Warn(msg string, fields ...Field) {\n\tl.zl.Warn(msg, fields...)\n}\n\n// Error level\nfunc (l *Logger) Error(msg string, fields ...Field) {\n\tl.zl.Error(msg, fields...)\n}\n\n// Fatal level\nfunc (l *Logger) Fatal(msg string, fields ...Field) {\n\tl.zl.Fatal(msg, fields...)\n}\n\n// 全局函数\n\n// With 创建带有字段的新Logger\nfunc With(key string, value interface{}) *Logger {\n\tl := GetLogger()\n\t// 为链式调用创建新的logger实例，并添加caller skip\n\treturn &Logger{zl: l.zl.WithOptions(zap.AddCallerSkip(1)).With(Any(key, value))}\n}\n\n// Debug level\nfunc Debug(msg string, fields ...Field) {\n\tl := GetLogger()\n\tl.zl.WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...)\n}\n\n// Info level\nfunc Info(msg string, fields ...Field) {\n\tl := GetLogger()\n\tl.zl.WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...)\n}\n\n// Warn level\nfunc Warn(msg string, fields ...Field) {\n\tl := GetLogger()\n\tl.zl.WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...)\n}\n\n// Error level\nfunc Error(msg string, fields ...Field) {\n\tl := GetLogger()\n\tl.zl.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)\n}\n\n// Fatal level\nfunc Fatal(msg string, fields ...Field) {\n\tl := GetLogger()\n\tl.zl.WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...)\n}\n"
  },
  {
    "path": "mcp_server/pkg/mcp/mcp.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/errors\"\n\t\"github.com/chaitin/SafeLine/mcp_server/pkg/logger\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/mcuadros/go-defaults\"\n)\n\ntype Tool[T any, R any] interface {\n\tName() string\n\n\tDescription() string\n\n\tExecute(ctx context.Context, params T) (R, error)\n\n\tValidate(params T) error\n}\n\ntype SSEServer struct {\n\tsse    *server.SSEServer\n\tsecret string\n}\n\nfunc (s *SSEServer) Start(addr string) error {\n\tsrv := &http.Server{\n\t\tAddr:    addr,\n\t\tHandler: s,\n\t}\n\treturn srv.ListenAndServe()\n}\n\nfunc (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif s.secret == \"\" {\n\t\ts.sse.ServeHTTP(w, r)\n\t\treturn\n\t}\n\tmessagePath := s.sse.CompleteMessagePath()\n\tif messagePath != \"\" && r.URL.Path == messagePath {\n\t\tsecret := r.Header.Get(\"Secret\")\n\t\tif secret != s.secret {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\ts.sse.ServeHTTP(w, r)\n}\n\ntype MCPServer struct {\n\tserver *server.MCPServer\n\tsse    *SSEServer\n}\n\nfunc NewMCPServer(name, version string, secret string) *MCPServer {\n\ts := server.NewMCPServer(\n\t\tname,\n\t\tversion,\n\t\tserver.WithLogging(),\n\t)\n\treturn &MCPServer{\n\t\tserver: s,\n\t\tsse:    &SSEServer{sse: server.NewSSEServer(s), secret: secret},\n\t}\n}\n\nfunc (s *MCPServer) Start(addr string) error {\n\treturn s.sse.Start(addr)\n}\n\nfunc handleToolCall[T any, R any](ctx context.Context, request mcp.CallToolRequest, tool Tool[T, R]) (result *mcp.CallToolResult, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"panic recovered: %v\", r)\n\t\t}\n\t}()\n\n\tvar raw []byte\n\traw, err = json.Marshal(request.Params.Arguments)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"marshal arguments failed\")\n\t}\n\tvar params T\n\tdefaults.SetDefaults(&params)\n\tif err = json.Unmarshal(raw, &params); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unmarshal parameters failed\")\n\t}\n\n\tif err = tool.Validate(params); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar execResult R\n\texecResult, err = tool.Execute(ctx, params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv := any(execResult)\n\tswitch v := v.(type) {\n\tcase string:\n\t\treturn mcp.NewToolResultText(v), nil\n\tcase []byte:\n\t\treturn mcp.NewToolResultText(string(v)), nil\n\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n\t\treturn mcp.NewToolResultText(json.Number(fmt.Sprint(v)).String()), nil\n\tcase bool:\n\t\treturn mcp.NewToolResultText(strconv.FormatBool(v)), nil\n\tdefault:\n\t\tbytes, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"invalid result type\")\n\t\t}\n\t\treturn mcp.NewToolResultText(string(bytes)), nil\n\t}\n}\n\nfunc RegisterTool[T any, R any](s *MCPServer, tool Tool[T, R]) error {\n\tvar v T\n\topts, err := SchemaToOptions(v)\n\tif err != nil {\n\t\treturn err\n\t}\n\topts = append(opts, mcp.WithDescription(tool.Description()))\n\tt := mcp.NewTool(tool.Name(),\n\t\topts...,\n\t)\n\n\ts.server.AddTool(t, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tresult, err := handleToolCall(ctx, request, tool)\n\t\tif err != nil {\n\t\t\tlogger.With(\"error\", err).Error(\"handle tool call failed\")\n\t\t\tif wrapped, ok := err.(*errors.Error); ok {\n\t\t\t\treturn nil, wrapped.Unwrap()\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "mcp_server/pkg/mcp/schema.go",
    "content": "package mcp\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\n// SchemaToOptions Convert struct to MCP ToolOption list\nfunc SchemaToOptions(schema any) ([]mcp.ToolOption, error) {\n\tt := reflect.TypeOf(schema)\n\tif t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\n\tvar options []mcp.ToolOption\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\t\tjsonTag := field.Tag.Get(\"json\")\n\t\tif jsonTag == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tdesc := field.Tag.Get(\"desc\")\n\t\trequired := field.Tag.Get(\"required\") == \"true\"\n\t\tenumTag := field.Tag.Get(\"enum\")\n\t\tdefaultTag := field.Tag.Get(\"default\")\n\t\tminTag := field.Tag.Get(\"min\")\n\t\tmaxTag := field.Tag.Get(\"max\")\n\t\topts := []mcp.PropertyOption{}\n\n\t\tif desc != \"\" {\n\t\t\topts = append(opts, mcp.Description(desc))\n\t\t}\n\t\tif required {\n\t\t\topts = append(opts, mcp.Required())\n\t\t}\n\t\tif enumTag != \"\" && field.Type.Kind() == reflect.String {\n\t\t\tenumValues := strings.Split(enumTag, \",\")\n\t\t\topts = append(opts, mcp.Enum(enumValues...))\n\t\t}\n\n\t\tswitch field.Type.Kind() {\n\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:\n\t\t\tif defaultTag != \"\" {\n\t\t\t\tif defaultValue, err := strconv.Atoi(defaultTag); err == nil {\n\t\t\t\t\topts = append(opts, mcp.DefaultNumber(float64(defaultValue)))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif minTag != \"\" {\n\t\t\t\tif minValue, err := strconv.Atoi(minTag); err == nil {\n\t\t\t\t\topts = append(opts, mcp.Min(float64(minValue)))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif maxTag != \"\" {\n\t\t\t\tif maxValue, err := strconv.Atoi(maxTag); err == nil {\n\t\t\t\t\topts = append(opts, mcp.Max(float64(maxValue)))\n\t\t\t\t}\n\t\t\t}\n\t\t\toptions = append(options, mcp.WithNumber(jsonTag, opts...))\n\t\tcase reflect.Bool:\n\t\t\tif defaultTag != \"\" {\n\t\t\t\tif defaultValue, err := strconv.ParseBool(defaultTag); err == nil {\n\t\t\t\t\topts = append(opts, mcp.DefaultBool(defaultValue))\n\t\t\t\t}\n\t\t\t}\n\t\t\toptions = append(options, mcp.WithBoolean(jsonTag, opts...))\n\t\tcase reflect.String:\n\t\t\tif defaultTag != \"\" {\n\t\t\t\topts = append(opts, mcp.DefaultString(defaultTag))\n\t\t\t}\n\t\t\toptions = append(options, mcp.WithString(jsonTag, opts...))\n\t\tcase reflect.Struct:\n\t\t\tsubSchema := reflect.New(field.Type).Interface()\n\t\t\tsubOptions, err := SchemaToOptions(subSchema)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\t// Create a temporary Tool to get JSON Schema of sub-struct\n\t\t\ttempTool := mcp.NewTool(\"temp\", subOptions...)\n\t\t\ttempJSON, _ := tempTool.MarshalJSON()\n\t\t\tvar tempMap map[string]any\n\t\t\tif err := json.Unmarshal(tempJSON, &tempMap); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Extract properties from temporary Tool\n\t\t\tif inputSchema, ok := tempMap[\"inputSchema\"].(map[string]any); ok {\n\t\t\t\tif properties, ok := inputSchema[\"properties\"].(map[string]any); ok {\n\t\t\t\t\t// Check if there are required fields\n\t\t\t\t\tif required, ok := inputSchema[\"required\"].([]any); ok {\n\t\t\t\t\t\t// Add required field information to corresponding properties\n\t\t\t\t\t\tfor _, req := range required {\n\t\t\t\t\t\t\tif reqStr, ok := req.(string); ok {\n\t\t\t\t\t\t\t\tif prop, ok := properties[reqStr].(map[string]any); ok {\n\t\t\t\t\t\t\t\t\tprop[\"required\"] = true\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\topts = append(opts, mcp.Properties(properties))\n\t\t\t\t}\n\t\t\t}\n\t\t\toptions = append(options, mcp.WithObject(jsonTag, opts...))\n\n\t\tcase reflect.Slice:\n\t\t\telemType := field.Type.Elem()\n\t\t\tvar items map[string]any\n\n\t\t\tswitch elemType.Kind() {\n\t\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:\n\t\t\t\titems = map[string]any{\n\t\t\t\t\t\"type\": \"number\",\n\t\t\t\t}\n\t\t\tcase reflect.Bool:\n\t\t\t\titems = map[string]any{\n\t\t\t\t\t\"type\": \"boolean\",\n\t\t\t\t}\n\t\t\tcase reflect.String:\n\t\t\t\titems = map[string]any{\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t}\n\t\t\tcase reflect.Struct:\n\t\t\t\tsubSchema := reflect.New(elemType).Interface()\n\t\t\t\tsubOptions, err := SchemaToOptions(subSchema)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\t// Create a temporary Tool to get JSON Schema of sub-struct\n\t\t\t\ttempTool := mcp.NewTool(\"temp\", subOptions...)\n\t\t\t\ttempJSON, _ := tempTool.MarshalJSON()\n\t\t\t\tvar tempMap map[string]any\n\t\t\t\tif err := json.Unmarshal(tempJSON, &tempMap); err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Extract properties from temporary Tool\n\t\t\t\tif inputSchema, ok := tempMap[\"inputSchema\"].(map[string]any); ok {\n\t\t\t\t\tif properties, ok := inputSchema[\"properties\"].(map[string]any); ok {\n\t\t\t\t\t\t// Check if there are required fields\n\t\t\t\t\t\tif required, ok := inputSchema[\"required\"].([]any); ok {\n\t\t\t\t\t\t\t// Add required field information to corresponding properties\n\t\t\t\t\t\t\tfor _, req := range required {\n\t\t\t\t\t\t\t\tif reqStr, ok := req.(string); ok {\n\t\t\t\t\t\t\t\t\tif prop, ok := properties[reqStr].(map[string]any); ok {\n\t\t\t\t\t\t\t\t\t\tprop[\"required\"] = true\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\titems = map[string]any{\n\t\t\t\t\t\t\t\"type\":       \"object\",\n\t\t\t\t\t\t\t\"properties\": properties,\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\topts = append(opts, mcp.Items(items))\n\t\t\toptions = append(options, mcp.WithArray(jsonTag, opts...))\n\t\t}\n\t}\n\n\treturn options, nil\n}\n"
  },
  {
    "path": "mcp_server/pkg/mcp/schema_test.go",
    "content": "package mcp\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nfunc TestSchemaToOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\targs any\n\t\twant mcp.Tool\n\t}{\n\t\t{\n\t\t\tname: \"test number\",\n\t\t\targs: struct {\n\t\t\t\tA int `json:\"a\" desc:\"number a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Required(), mcp.Description(\"number a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test number int64\",\n\t\t\targs: struct {\n\t\t\t\tA int64 `json:\"a\" desc:\"number a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number int64\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Required(), mcp.Description(\"number a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test number float64\",\n\t\t\targs: struct {\n\t\t\t\tA float64 `json:\"a\" desc:\"number a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number float64\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Required(), mcp.Description(\"number a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test number default\",\n\t\t\targs: struct {\n\t\t\t\tA int `json:\"a\" desc:\"number a\" required:\"true\" default:\"10\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number default\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Required(), mcp.Description(\"number a\"), mcp.DefaultNumber(10)),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test number min max\",\n\t\t\targs: struct {\n\t\t\t\tA int `json:\"a\" desc:\"number a\" required:\"true\" min:\"10\" max:\"20\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number min max\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Required(), mcp.Description(\"number a\"), mcp.Min(10), mcp.Max(20)),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test number optional\",\n\t\t\targs: struct {\n\t\t\t\tA int `json:\"a\" desc:\"number a\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test number optional\",\n\t\t\t\tmcp.WithNumber(\"a\", mcp.Description(\"number a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test boolean\",\n\t\t\targs: struct {\n\t\t\t\tA bool `json:\"a\" desc:\"boolean a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test boolean\",\n\t\t\t\tmcp.WithBoolean(\"a\", mcp.Required(), mcp.Description(\"boolean a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test string\",\n\t\t\targs: struct {\n\t\t\t\tA string `json:\"a\" desc:\"string a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test string\",\n\t\t\t\tmcp.WithString(\"a\", mcp.Required(), mcp.Description(\"string a\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test string default\",\n\t\t\targs: struct {\n\t\t\t\tA string `json:\"a\" desc:\"string a\" required:\"true\" default:\"hello\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test string default\",\n\t\t\t\tmcp.WithString(\"a\", mcp.Required(), mcp.Description(\"string a\"), mcp.DefaultString(\"hello\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test string enum\",\n\t\t\targs: struct {\n\t\t\t\tA string `json:\"a\" desc:\"string a\" required:\"true\" enum:\"1,2,3\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test string enum\",\n\t\t\t\tmcp.WithString(\"a\", mcp.Required(), mcp.Description(\"string a\"), mcp.Enum(\"1\", \"2\", \"3\")),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test object\",\n\t\t\targs: struct {\n\t\t\t\tA struct {\n\t\t\t\t\tB int `json:\"b\" desc:\"number b\" required:\"true\"`\n\t\t\t\t} `json:\"a\" desc:\"object a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test object\",\n\t\t\t\tmcp.WithObject(\"a\", mcp.Required(), mcp.Description(\"object a\"),\n\t\t\t\t\tmcp.Properties(map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"type\":        \"number\",\n\t\t\t\t\t\t\t\"description\": \"number b\",\n\t\t\t\t\t\t\t\"required\":    true,\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\t{\n\t\t\tname: \"test object optional\",\n\t\t\targs: struct {\n\t\t\t\tA struct {\n\t\t\t\t\tB int `json:\"b\" desc:\"number b\" required:\"true\"`\n\t\t\t\t\tC int `json:\"c\" desc:\"number c\"`\n\t\t\t\t} `json:\"a\" desc:\"object a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test object optional\",\n\t\t\t\tmcp.WithObject(\"a\", mcp.Required(), mcp.Description(\"object a\"),\n\t\t\t\t\tmcp.Properties(map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"type\":        \"number\",\n\t\t\t\t\t\t\t\"description\": \"number b\",\n\t\t\t\t\t\t\t\"required\":    true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"c\": map[string]any{\n\t\t\t\t\t\t\t\"type\":        \"number\",\n\t\t\t\t\t\t\t\"description\": \"number c\",\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\t{\n\t\t\tname: \"test nested object\",\n\t\t\targs: struct {\n\t\t\t\tA struct {\n\t\t\t\t\tB struct {\n\t\t\t\t\t\tC int `json:\"c\" desc:\"number c\" required:\"true\"`\n\t\t\t\t\t} `json:\"b\" desc:\"object b\" required:\"true\"`\n\t\t\t\t} `json:\"a\" desc:\"object a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test nested object\",\n\t\t\t\tmcp.WithObject(\"a\", mcp.Required(), mcp.Description(\"object a\"),\n\t\t\t\t\tmcp.Properties(map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"type\":        \"object\",\n\t\t\t\t\t\t\t\"description\": \"object b\",\n\t\t\t\t\t\t\t\"required\":    true,\n\t\t\t\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\t\t\t\"c\": map[string]any{\n\t\t\t\t\t\t\t\t\t\"type\":        \"number\",\n\t\t\t\t\t\t\t\t\t\"description\": \"number c\",\n\t\t\t\t\t\t\t\t\t\"required\":    true,\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\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test array\",\n\t\t\targs: struct {\n\t\t\t\tA []int `json:\"a\" desc:\"array a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test array\",\n\t\t\t\tmcp.WithArray(\"a\", mcp.Required(), mcp.Description(\"array a\"),\n\t\t\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\t\t\"type\": \"number\",\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"test array of object\",\n\t\t\targs: struct {\n\t\t\t\tA []struct {\n\t\t\t\t\tB int `json:\"b\" desc:\"number b\" required:\"true\"`\n\t\t\t\t} `json:\"a\" desc:\"array of object a\" required:\"true\"`\n\t\t\t}{},\n\t\t\twant: mcp.NewTool(\"test array of object\",\n\t\t\t\tmcp.WithArray(\"a\", mcp.Required(), mcp.Description(\"array of object a\"),\n\t\t\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\t\"type\":        \"number\",\n\t\t\t\t\t\t\t\t\"description\": \"number b\",\n\t\t\t\t\t\t\t\t\"required\":    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},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := SchemaToOptions(tt.args)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"SchemaToOptions() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts1, _ := mcp.NewTool(tt.name, got...).MarshalJSON()\n\t\t\ts2, _ := tt.want.MarshalJSON()\n\t\t\tif !reflect.DeepEqual(s1, s2) {\n\t\t\t\tt.Errorf(\"\\n got  %v\\n want %v\", string(s1), string(s2))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scripts/manage.py",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport base64\nimport json\nimport shutil\nimport ssl\nimport sys\nimport datetime\nimport platform\nimport os\nfrom urllib.request import urlopen, HTTPError\nimport re\nimport subprocess\nimport string\nimport time\nimport random\nimport socket\n\n\ntexts = {\n    'hello1': {\n        'en': 'SafeLine is a self-hosted WAF(Web Application Firewall) to protect your web apps from attacks and exploits.',\n        'zh': 'SafeLine，中文名 \"雷池\"，是一款简单好用, 效果突出的 Web 应用防火墙(WAF)，可以保护 Web 服务不受黑客攻击。'\n    },\n    'hello2': {\n        'en': 'A web application firewall helps protect web apps by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web apps from attacks such as SQL injection, XSS, code injection, os command injection, CRLF injection, ldap injection, xpath injection, RCE, XXE, SSRF, path traversal, backdoor, bruteforce, http-flood, bot abused, among others.',\n        'zh': '雷池通过过滤和监控 Web 应用与互 联网之间的 HTTP 流量来保护 Web 服务。可以保护 Web 服务免受 SQL 注入、XSS 、 代码注入、命令注入、CRLF 注入、ldap 注入、xpath 注入、RCE、XXE、SSRF、路径遍历、后门、暴力破解、CC、爬虫 等攻击。'\n    },\n    'talking-group': {\n        'en': '\\n'\n              'https://discord.gg/SVnZGzHFvn\\n'\n              '\\n'\n              'Join discord group for more informations of SafeLine by above address',\n        'zh': '\\n'\n              '▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\\n'\n              '█ ▄▄▄▄▄ █▀ █▀▀██▀▄▀▀▄▀▄▀▄██ ▄▄▄▄▄ █\\n'\n              '█ █   █ █▀ ▄ █▀▄▄▀▀ ▄█▄  ▀█ █   █ █\\n'\n              '█ █▄▄▄█ █▀█ █▄█▄▀▀▄▀▄ ▀▀▄▄█ █▄▄▄█ █\\n'\n              '█▄▄▄▄▄▄▄█▄█▄█ █▄▀ █ ▀▄▀ █▄█▄▄▄▄▄▄▄█\\n'\n              '█▄ ▄▄ █▄▄  ▄█▄▄▄▄▀▄▀▀▄██ ▄▄▀▄█▄▀ ▀█\\n'\n              '█▄ ▄▀▄ ▄▀▄ ▀ ▄█▀ ▀▄ █▀▀ ▀█▀▄██▄▀▄██\\n'\n              '██ ▀▄█ ▄ ▄▄▀▄▀▀█▄▀▄▄▀▄▀▄ ▄ ▀▄▄▄█▀▀█\\n'\n              '█ █▀▄▀ ▄▀▄▄▀█▀ ▄▄ █▄█▀▀▄▀▀█▄█▄█▀▄██\\n'\n              '█ █ ▀  ▄▀▀ ██▄█▄▄▄▄▄▀▄▀▀▀▄▄▀█▄▀█ ▀█\\n'\n              '█ █ ▀▄ ▄██▀▀ ▄█▀ ▀███▄  ▀▄▀▄▄ ▄▀▄██\\n'\n              '█▀▄▄█  ▄▀▄▀ ▄▀▀▀▄▀▄▀ ▄▀▄  ▄▀ ▄▀█ ▀█\\n'\n              '█ █ █ █▄▀ █▄█▀ ▄▄███▀▀▀▄█▀▄ ▀  ▀▄██\\n'\n              '█▄███▄█▄▄▀▄ █▄█▄▄▄▄▀▀▄█▀▀ ▄▄▄  ▀█ █\\n'\n              '█ ▄▄▄▄▄ █▄▀█ ▄█▀▄ █▀█▄ ▀  █▄█  ▀▄▀█\\n'\n              '█ █   █ █  █▄▀▀▀▄▄▄▀▀▀▀▀▀ ▄▄  ▀█  █\\n'\n              '█ █▄▄▄█ █  ▀█▀ ▄▄▄▄ ▀█ ▀▀▄▀ ▀▀ ▀███\\n'\n              '█▄▄▄▄▄▄▄█▄▄██▄█▄▄█▄██▄██▄▄█▄▄█▄█▄██\\n'\n              '\\n'\n              '微信扫描上方二维码加入雷池项目讨论组'\n    },\n    'input-target-path': {\n        'en': 'Input the path to install %s',\n        'zh': '请输入%s 的安装目录'\n    },\n    'input-mgt-port': {\n        'en': 'Input the %s mgt port',\n        'zh': '请输入%s 的管理端口'\n    },\n    'python-version-too-low': {\n        'en': 'The Python version is too low, Python 3.5 above is required',\n        'zh': 'Python 版本过低, 脚本无法运行, 需要 Python 3.5 以上'\n    },\n    'not-a-tty': {\n        'en': 'Stdin is not a standard TTY',\n        'zh': '运行脚本的方式不对, STDIN 不是标准的 TTY'\n    },\n    'not-root': {\n        'en': 'Requires root privileges to run',\n        'zh': '需要 root 权限才能运行'\n    },\n    'not-linux': {\n        'en': '%s does not support %s OS yet',\n        'zh': '%s 暂时不支持 %s 操作系统'\n    },\n    'unsupported-arch': {\n        'en': '%s does not support %s processor yet',\n        'zh': '%s 暂时不支持 %s 处理器'\n    },\n    'prepare-to-install': {\n        'en': 'Will be going to installing %s for you.',\n        'zh': '即将为您安装%s'\n    },\n    'choice-action': {\n        'en': 'Choice what do you want to do',\n        'zh': '选择你要执行的动作'\n    },\n    'default-value': {\n        'en': 'Keep blank default to',\n        'zh': '留空则为默认值'\n    },\n    'ssse3-not-support': {\n        'en': 'SSSE3 instruction set not enabled in your CPU',\n        'zh': '当前 CPU 未启用 SSSE3 指令集'\n    },\n    'precheck-failed': {\n        'en': 'The environment does not meet the installation conditions of %s',\n        'zh': '当前环境不符合%s 的安装条件'\n    },\n    'precheck-passed': {\n        'en': 'Installation environment check passed',\n        'zh': '检查安装环境已完成'\n    },\n    'insufficient-memory': {\n        'en': 'Remaining memory is less than 1 GB',\n        'zh': '剩余内存不足 1 GB'\n    },\n    'docker-not-installed': {\n        'en': 'Running %s requires Docker, but Docker is not installed',\n        'zh': '运行%s 依赖 Docker, 但是 Docker 没安装'\n    },\n    'docker-compose-not-installed': {\n        'en': 'Running %s requires Docker Compose, but Docker Compose is not installed',\n        'zh': '运行%s 依赖 Docker Compose, 但是 Docker Compose 没安装'\n    },\n    'docker-version-too-low': {\n        'en': 'Docker version is too low, it does not match %s',\n        'zh': 'Docker 版本太低, 不满足%s 的安装需求'\n    },\n    'if-install-docker': {\n        'en': 'Do you want the latest version of Docker to be automatically installed for you?',\n        'zh': '是否需要为你自动安装 Docker 的最新版本'\n    },\n    'if-restart-docker': {\n        'en': 'Do you want to restart %s Docker container',\n        'zh': '是否需要重启%s 的容器'\n    },\n    'if-update-docker': {\n        'en': 'Do you want to update your Docker version?',\n        'zh': '是否需要为你自动更新 Docker 版本'\n    },\n    'install-docker-failed': {\n        'en': 'Failed to install Docker. Please try to install Docker manually before installing %s',\n        'zh': '安装 Docker 失败, 请尝试手动安装 Docker 后再来安装%s'\n    },\n    'install-docker': {\n        'en': 'Docker is being installed for you. It will take a few minutes. Please wait patiently.',\n        'zh': '正在为你安装 Docker, 需要几分钟时间, 请耐心等待'\n    },\n    'get-space-failed': {\n        'en': 'Unable to query disk capacity of \"%s\"',\n        'zh': '无法查询 \"%s\" 的磁盘容量'\n    },\n    'remain-disk-capacity': {\n        'en': 'Disk capacity of \"%s\" has %s avaiable',\n        'zh': '\"%s\" 路径有 %s 的空间可用'\n    },\n    'insufficient-disk-capacity': {\n        'en': 'Insufficient disk capacity of \"%s\", at least 5 GB is required to install %s',\n        'zh': '\"%s\" 的磁盘容量不足，安装%s 至少需要 5 GB'\n    },\n    'pg-pass-contains-invalid-char': {\n        'en': 'The POSTGRES_PASSWORD variable contains special characters. Please choose repair to reset password',\n        'zh': 'POSTGRES_PASSWORD 变量包含特殊字符, 请选择修复重置密码'\n    },\n    'invalid-path': {\n        'en': '\"%s\" is not a valid absolute path',\n        'zh': '\"%s\" 不是合法的绝对路径'\n    },\n    'path-exists': {\n        'en': '\"%s\" already exists, please select a new directory',\n        'zh': '路径 \"%s\" 已存在，请选择一个全新的目录'\n    },\n    'fail-to-parse-route': {\n        'en': 'Unable to parse /proc/net/route file',\n        'zh': '无法解析 /proc/net/route 文件'\n    },\n    'fail-to-download-compose': {\n        'en': 'Failed to download docker compose script',\n        'zh': '下载 docker compose 脚本失败'\n    },\n    'fail-to-create-dir': {\n        'en': 'Unable to create the \"%s\" directory',\n        'zh': '无法创建 \"%s\" 目录'\n    },\n    'docker-pull': {\n        'en': 'Pulling Docker image',\n        'zh': '正在拉取 Docker 镜像'\n    },\n    'try-another-image-source': {\n        'en': 'Try another image source',\n        'zh': '尝试使用其他镜像源'\n    },\n    'image-clean': {\n        'en': 'Cleaning Docker image',\n        \"zh\": '正在清理 Docker 镜像'\n    },\n    'update-config': {\n        'en': 'Updating .env configuration files',\n        'zh': '正在更新 .env 配置文件'\n    },\n    'download-compose': {\n        'en': 'Downloading the docker-compose.yaml file',\n        'zh': '正在下载 docker-compose.yaml 文件'\n    },\n    'fail-to-pull-image': {\n        'en': 'Failed to pull Docker image',\n        'zh': '拉取 Docker 镜像失败'\n    },\n    'docker-up': {\n        'en': 'Starting Docker containers',\n        'zh': '正在启动 Docker 容器'\n    },\n    'fail-to-up': {\n        'en': 'Failed to start Docker containers',\n        'zh': '启动 Docker 容器失败'\n    },\n    'fail-to-down': {\n        'en': 'Failed to stop Docker containers',\n        'zh': '停止 Docker 容器失败'\n    },\n    'install-finish': {\n        'en': '%s installation completed',\n        'zh': '%s 安装完成'\n    },\n    'upgrade-finish': {\n        'en': '%s upgrade completed',\n        'zh': '%s 升级完成'\n    },\n    'go-to-panel': {\n        'en': '%s management panel: https://%s:%s/',\n        'zh': '%s 管理面板: https://%s:%s/'\n    }\n    ,'install': {\n        'en': 'INSTALL',\n        'zh': '安装'\n    },\n    'repair': {\n        'en': 'REPAIR',\n        'zh': '修复'\n    },\n    'uninstall': {\n        'en': 'UNINSTALL',\n        'zh': '卸载'\n    },\n    'upgrade': {\n        'en': 'UPGRADE',\n        'zh': '升级'\n    },\n    'backup': {\n        'en': 'BACKUP',\n        'zh': '备份'\n    },\n    'yes': {\n        'en': 'Yes',\n        'zh': '是'\n    },\n    'no': {\n        'en': 'No',\n        'zh': '否'\n    },\n    'fail-to-get-installed-dir': {\n        'en': 'Failed to get installed dir',\n        'zh': '未能找到安装目录',\n    },\n    'fail-to-connect-image-source': {\n        'en': 'Failed to connect image source',\n        'zh': '无法连接到任何镜像源'\n    },\n    'fail-to-connect-docker-source': {\n        'en': 'Failed to connect docker source',\n        'zh': '无法连接到任何 docker 源'\n    },\n    'fail-to-download-docker-installation': {\n        'en': 'Failed to download docker installation',\n        \"zh\": '下载 docker 安装脚本失败'\n    },\n    'docker-source': {\n        'en': 'Docker source',\n        \"zh\": 'docker 源'\n    },\n    'reset-admin': {\n        'en': 'Setup admin',\n        \"zh\": '设置 admin'\n    },\n    'docker-version': {\n        'en': 'Checking docker version',\n        'zh': '检查 docker 版本'\n    },\n    'docker-compose-version': {\n        'en': 'Checking docker compose version',\n        'zh': '检查 docker compose 版本'\n    },\n    'keyboard-interrupt': {\n        'en': 'Installation cancelled',\n        \"zh\": '取消安装'\n    },\n    'docker-up-iptables-failed': {\n        'en': 'Iptables policy error, try to restart docker',\n        'zh': 'iptables 规则错误，尝试重启 docker'\n    },\n    'install-channel': {\n        'en': 'Installing: %s',\n        'zh': '安装通道：%s'\n    },\n    'preview-release': {\n        'en': 'Preview',\n        'zh': '预览版'\n    },\n    'lts-release': {\n        'en': 'LTS',\n        'zh': 'LTS 版'\n    },\n    'fail-to-docker-down': {\n        'en': 'Failed to stop container',\n        'zh': '停止 docker 容器失败'\n    },\n    'fail-to-remove-dir': {\n        'en': 'Failed to remove %s installation directory',\n        'zh': '删除 %s 安装目录失败'\n    },\n    'uninstall-finish': {\n        'en': '%s uninstall completed',\n        'zh': '%s 卸载完成'\n    },\n    'docker-down': {\n        'en': 'Stopping %s container',\n        'zh': '正在停止%s 容器'\n    },\n    'reset-tengine': {\n        'en': 'RESET TENGINE CONFIG',\n        'zh': '重置 tengine 配置',\n    },\n    'reset-postgres': {\n        'en': 'RESET DATABASE PASSWORD',\n        'zh': '重置数据库密码'\n    },\n    'fail-to-find-nginx': {\n        'en': 'Failed to find tengine config path',\n        'zh': '未找到 tengine 配置目录'\n    },\n    'nginx-backup-dir': {\n        'en': 'Tengine config backup directory',\n        'zh': 'tengine 配置备份目录'\n    },\n    'fail-to-backup-nginx': {\n        'en': 'Failed to backup tengine config',\n        'zh': '备份 tengine 目录失败'\n    },\n    'docker-restart': {\n        'en': 'Restart docker container',\n        'zh': '重启 docker 容器'\n    },\n    'docker-exec': {\n        'en': 'Executing docker command',\n        'zh': '执行 docker 命令'\n    },\n    'fail-to-recover-static': {\n        'en': 'Failed to recover tengine static config',\n        'zh': '恢复 tengine 静态站点资源失败'\n    },\n    'fail-to-find-env': {\n        'en': 'Failed to find .env file',\n        'zh': '未找到 .env 文件'\n    },\n    'fail-to-find-postgres-password': {\n        'en': 'Failed to find postgres password',\n        'zh': '未找到数据库密码'\n    },\n    'fail-to-reset-postgres-password': {\n        'en': 'Failed to reset postgres password',\n        'zh': '重置数据库密码失败'\n    },\n    'reset-postgres-password-finish': {\n        'en': 'Reset postgres password completed',\n        'zh': '重置数据库密码完成'\n    },\n    'reset-tengine-finish': {\n        'en': 'Reset tengine finish completed',\n        'zh': '重置 tengine 配置完成'\n    },\n    'if-remove-waf': {\n        'en': 'Do you want to uninstall %s, this operation will delete all data in the directory: %s',\n        'zh': '是否确认卸载%s，该操作会删除目录下所有数据：%s'\n    },\n    'restart-docker-finish': {\n        'en': 'Restart %s docker container completed',\n        'zh': '重启%s 容器完成'\n    },\n    'restart': {\n        'en': 'RESTART',\n        'zh': '重启'\n    },\n    'wait-mgt-health': {\n        'en': 'Wait for mgt healthy',\n        'zh': '等待 mgt 启动'\n    },\n    'if-remove-ipv6-scope': {\n        'en': '/etc/resolv.conf have ipv6 nameserver with scope. Do you want to remove these nameserver',\n        'zh': '/etc/resolv.conf 文件中存在 ipv6 地址包含区域 ID，是否删除包含区域 ID 的 ipv6 nameserver'\n    },\n    'input-target-version': {\n        'en': 'Input the %s version',\n        'zh': '请输入 %s 的版本'\n    },\n    'version-format-error': {\n        'en': 'version %s format error',\n        'zh': '版本 %s 格式错误'\n    },\n    'can-not-downgrade': {\n        'en': '%s can not downgrade %s to %s',\n        'zh': '%s 不支持从 %s 降级到 %s'\n    },\n    'get-version': {\n        'en': 'Getting %s latest version',\n        'zh': '正在获取%s 最新版本'\n    },\n    'get-version-from-mgt': {\n        'en': 'try to get version from mgt',\n        'zh': '尝试从 mgt 获取安装版本'\n    },\n    'skip-version-compare': {\n        'en': 'skip %s version compare',\n        'zh': '跳过%s 版本匹配'\n    },\n    'fail-to-get-version-from-mgt': {\n        'en': 'failed to get install version from mgt',\n        'zh': '从 mgt 获取安装版本失败'\n    },\n    'target-version': {\n        'en': 'target version: %s',\n        'zh': '目标版本：%s'\n    },\n    'fail-to-parse-assets': {\n        'en': 'failed to parse assets',\n        'zh': '解析补丁包失败'\n    },\n    'snap-docker-should-use-home-path': {\n        'en': 'Docker installed via snap can only be configured to set the installation directory under the user\\'s home directory(%s)',\n        'zh': 'snap 安装的 docker 只能设置安装目录在用户的主目录(%s)下'\n    },\n    'fail-to-get-docker-path': {\n        'en': 'find docker binary path failed',\n        'zh': '获取 docker 二进制目录失败'\n    }\n}\n\n\nBOLD    = 1\nDIM     = 1\nBLINK   = 5\nREVERSE = 7\nRED     = 31\nGREEN   = 32\nYELLOW  = 33\nBLUE    = 34\nCYAN    = 36\n\nINSTALL = False\nDOMAIN = 'waf-ce.chaitin.cn'\nREQUEST_CTX = None\nLANG = 'zh'\nPRODUCT = ''\nDEBUG = False\nSELF = True\n\ndef parse_assets(args):\n    global PRODUCT, SELF\n\n    if args.patch == '':\n        return True\n    assets = ''\n    with open(args.patch, 'r') as f:\n        for line in f.readlines():\n            assets += line\n\n    split_assets = assets.strip().split('.')\n    if len(split_assets) != 3:\n        return False\n\n    try:\n        assets_info = json.loads(base64.b64decode(split_assets[1].replace('_','/').replace('-','+') + '=' * ((4 - len(split_assets[1]) % 4) % 4)))\n        if args.en:\n            PRODUCT = assets_info['fullname_en']\n        else:\n            PRODUCT = assets_info['fullname']\n\n        if PRODUCT == '':\n            return False\n\n        if assets_info['self'] is None:\n            return False\n\n        if  not assets_info['self']:\n            SELF = False\n    except Exception as e:\n        log.debug('parse assets failed: ' + str(e))\n        return False\n\n    return True\n\ndef init_global_config():\n    global REQUEST_CTX, LANG, DOMAIN, PRODUCT, DEBUG\n\n    REQUEST_CTX = ssl.create_default_context()\n    REQUEST_CTX.check_hostname = False\n    REQUEST_CTX.verify_mode = ssl.CERT_NONE\n\n    parser = argparse.ArgumentParser(\n        prog='installer-management',\n        description='installer-management',\n        allow_abbrev=False\n    )\n    parser.add_argument('--debug', action='store_true', help='install with debug log')\n    parser.add_argument(\"--lts\", action='store_true', help='install lts version')\n    parser.add_argument('--image-clean', action='store_true', help='clean image when upgrade done')\n    parser.add_argument('--en', action='store_true', help='install international version')\n    parser.add_argument('--patch', default='', type=str, help='patch path')\n    args = parser.parse_args()\n    if args.en:\n        LANG = 'en'\n        DOMAIN = 'waf.chaitin.com'\n        PRODUCT = 'SafeLine WAF'\n    else:\n        PRODUCT = '雷池 WAF'\n\n    if args.debug:\n        DEBUG = True\n\n    if args.patch != '' and not os.path.exists(args.patch):\n        log.fatal('assets %s not exists' % args.patch)\n\n    if not parse_assets(args):\n        log.fatal(text('fail-to-parse-assets'))\n    return args\n\nclass log():\n    @staticmethod\n    def _log(c, l, s):\n        t = datetime.datetime.now().strftime('%H:%M:%S')\n        print('\\r\\033[0;%dm[%-5s %s]: %s\\033[0m' % (c, l, t, s))\n\n    @staticmethod\n    def debug(s):\n        if DEBUG:\n            log._log(DIM, 'DEBUG', s)\n\n    @staticmethod\n    def info(s):\n        log._log(CYAN, 'INFO', s)\n\n    @staticmethod\n    def warning(s):\n        log._log(YELLOW, 'WARN', s)\n\n    @staticmethod\n    def error(s):\n        log._log(RED, 'ERROR', s)\n\n    @staticmethod\n    def fatal(s):\n        log._log(RED, 'ERROR', s)\n        sys.exit(1)\n\ndef text(label, var=()):\n    t = texts.get(label, {\n        'en': 'Unknown \"%s\" (%s)' % (label, var),\n        'zh': '未知变量 \"%s\" (%s)'  % (label, var)\n    })\n    return t[LANG if LANG in t else 'en'] % var\n\ndef color(t, attrs=[], end=True):\n    t = '\\x1B[%sm%s' % (';'.join([str(i) for i in attrs]), t)\n    if end:\n        t = t + '\\x1B[m'\n    return t\n\nGLOBAL_ARGS = init_global_config()\n\ndef banner():\n    t = r'''\n  ______               ___           _____       _                        ____      ____       _        ________\n.' ____ \\            .' ..]         |_   _|     (_)                      |_  _|    |_  _|     / \\      |_   __  |\n| (___ \\_|  ,--.    _| |_    .---.    | |       __    _ .--.    .---.      \\ \\  /\\  / /      / _ \\       | |_ \\_|\n _.____`.  `'_\\ :  '-| |-'  / /__\\\\   | |   _  [  |  [ `.-. |  / /__\\\\      \\ \\/  \\/ /      / ___ \\      |  _|\n| \\____) | // | |,   | |    | \\__.,  _| |__/ |  | |   | | | |  | \\__.,       \\  /\\  /     _/ /   \\ \\_   _| |_\n \\______.' \\'-;__/  [___]    '.__.' |________| [___] [___||__]  '.__.'        \\/  \\/     |____| |____| |_____|\n\n'''.strip('\\n')\n    print(color(t + '\\n', [GREEN, BLINK]))\n\ndef get_url(url):\n    try:\n        response = urlopen(url, timeout=10, context=REQUEST_CTX)\n        content = response.read()\n        return content.decode('utf-8')\n    except Exception as e:\n        log.error('get url %s failed: %s' % (url, str(e)))\n\ndef ui_read(question, default):\n    while True:\n        if default is None:\n            sys.stdout.write('%s: ' % (\n                color(question, [GREEN]),\n            ))\n        else:\n            sys.stdout.write('%s  %s: ' % (\n                color(question, [GREEN]),\n                color('(%s %s)' % (text('default-value'), default), [YELLOW]),\n                ))\n        r = input().strip()\n        if len(r) == 0:\n            if default is None or len(default) == 0:\n                continue\n            r = default\n        return r\n\ndef ui_choice(question, options):\n    while True:\n        s_options = '[ %s ]' % '  '.join(['%s.%s' % option for option in options])\n        s_choices = '(%s)' % '/'.join([option[0] for option in options])\n        sys.stdout.write('%s  %s  %s: ' % (color(question, [GREEN]), color(s_options, [YELLOW]), color(s_choices, [YELLOW])))\n        r = input().strip()\n        if r in [i[0] for i in options]:\n            return r\n\ndef humen_size(x):\n    if x >= 1024 * 1024 * 1024 * 1024:\n        return '%.02f TB' % (x / 1024 / 1024 / 1024 / 1024)\n    elif x >= 1024 * 1024 * 1024:\n        return '%.02f GB' % (x / 1024 / 1024 / 1024)\n    elif x >= 1024 * 1024:\n        return '%.02f MB' % (x / 1024 / 1024)\n    elif x >= 1024:\n        return '%.02f KB' % (x / 1024)\n    else:\n        return '%d Bytes'\n\ndef rand_subnet():\n    routes = []\n    try:\n        with open('/proc/net/route', 'r') as f:\n            next(f)\n            for line in f:\n                parts = line.split()\n                if len(parts) < 8:\n                    continue\n                destination = int(parts[1], 0x10)\n                if destination == 0:\n                    continue\n                mask = int(parts[7], 0x10)\n                routes.append((destination, mask))\n    except Exception as e:\n        log.error(text('fail-to-parse-route') + ' ' + str(e))\n    for i in range(256):\n        t = 192\n        t += 168 << 8\n        t += i << 16\n        for route in routes:\n            if t & route[1] == route[0]:\n                break\n        else:\n            return '%d.%d.%d' % (t & 0xFF, (t >> 8) & 0xFF, (t >> 16) & 0xFF)\n    return '172.22.222'\n\ndef free_space(path):\n    while not os.path.exists(path) and path != '/':\n        path = os.path.dirname(path)\n    try:\n        st = os.statvfs(path)\n        free_bytes = st.f_bavail * st.f_frsize\n        return free_bytes\n    except Exception as e:\n        log.error(text('get-space-failed', path) + ' ' + str(e))\n        return None\n\ndef free_memory():\n    t = filter(lambda x: 'MemAvailable' in x, open('/proc/meminfo', 'r').readlines())\n    return int(next(t).split()[1]) * 1024\n\ndef exec_command(*args,shell=False):\n    try:\n        proc = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,shell=shell)\n        subprocess_output(proc.stdout.strip())\n        return proc.returncode, proc.stdout, proc.stderr\n    except Exception as e:\n        return -1, '', str(e)\n\ndef exec_command_with_loading(*args, cwd=None, env=None):\n    try:\n        with subprocess.Popen(args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=env, cwd=cwd) as proc:\n            if not DEBUG:\n                loading = [\"⣾\", \"⣽\", \"⣻\", \"⢿\", \"⡿\", \"⣟\", \"⣯\", \"⣷\"]\n                iloading = 0\n                while proc.poll() is None:\n                    sys.stderr.write('\\r' + loading[iloading])\n                    sys.stderr.flush()\n                    iloading = (iloading + 1) % len(loading)\n                    time.sleep(0.1)\n                sys.stderr.write('\\r')\n                sys.stderr.flush()\n            else:\n                for line in iter(proc.stdout.readline, b''):\n                    if line.strip() != '':\n                        log.debug(\"  -->> \"+line.strip())\n                    if proc.poll() is not None and line == '':\n                        break\n            return proc.returncode, proc.stdout.read(), proc.stderr.read()\n    except Exception as e:\n        return -1, '', str(e)\n\ndef subprocess_output(stdout):\n    if stdout != '':\n        log.debug(\"  -->> \"+stdout)\n    else:\n        log.debug(\"  -->> subprocess empty output\")\n\ndef start_docker():\n    return exec_command('systemctl enable docker && systemctl daemon-reload && systemctl restart docker',shell=True)\n\ndef check_port(port):\n    if not INSTALL:\n        return True\n\n    try:\n        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n            s.bind(('0.0.0.0', int(port)))\n            return True\n    except Exception as e:\n        log.debug(\"try listen mgt port failed: \"+str(e))\n        return False\n\ndef install_docker():\n    log.info(text('install-docker'))\n\n    log.debug(\"downloading get-docker.sh\")\n    if not save_file_from_url('https://'+DOMAIN+'/release/latest/get-docker.sh','get-docker.sh'):\n        raise Exception(text('fail-to-download-docker-installation'))\n\n    source = docker_source()\n    if source == '':\n        raise Exception(text('fail-to-connect-docker-source'))\n\n    log.debug(text('docker-source')+': '+source)\n    env = {\n        \"DOWNLOAD_URL\": source,\n        \"https_proxy\": os.environ.get('https_proxy',''),\n    }\n    log.debug(\"installing docker\")\n    r = exec_command_with_loading('bash get-docker.sh',env=env)\n    if r[0] == 0:\n        p = start_docker()\n        subprocess_output(p[1].strip())\n        if p[0] != 0:\n            log.error(\"start docker failed: \"+p[2].strip())\n        return p[0] == 0\n    else:\n        log.error(\"install docker error: \"+r[2].strip())\n    return False\n\ncompose_command = ''\n\ndef precheck_docker_compose():\n    log.info(text(\"docker-compose-version\"))\n    global compose_command\n\n    while True:\n        version_output = ''\n        proc = exec_command('docker', 'compose', 'version')\n        if proc[0] == 0:\n            help_proc = exec_command('docker', 'compose', 'up', '--help')\n            if help_proc[0] == 0 and '--detach' in help_proc[1]:\n                compose_command = 'docker compose -f docker-compose.yaml'\n                version_output = proc[1].strip()\n            else:\n                log.debug('docker compose can not find detach argument')\n        else:\n            compose_proc = exec_command('docker-compose', 'version')\n            if compose_proc[0] == 0:\n                help_proc = exec_command('docker-compose', 'up', '--help')\n                if help_proc[0] == 0 and '--detach' in help_proc[1]:\n                    compose_command = 'docker-compose -f docker-compose.yaml'\n                    version_output = compose_proc[1].strip()\n                else:\n                    log.debug('docker-compose can not find detach argument')\n            else:\n                log.warning(text('docker-compose-not-installed', PRODUCT))\n\n        if version_output != '':\n            t = re.findall(r'^Docker Compose version v?(\\d+)\\.', version_output)\n            if len(t) == 0:\n                log.warning(text('docker-compose-not-installed', PRODUCT))\n            elif int(t[0]) < 2:\n                log.warning(text('docker-version-too-low', PRODUCT))\n            else:\n                return True\n\n        action = ui_choice(text('if-update-docker'), [\n            ('y', text('yes')),\n            ('n', text('no')),\n        ])\n        if action.lower() == 'n':\n            return False\n        elif action.lower() == 'y':\n            if not install_docker():\n                log.warning(text('install-docker-failed', PRODUCT))\n                return False\n\ndef precheck_dns_scope():\n    resolve_file = '/etc/resolv.conf'\n    if not os.path.exists(resolve_file):\n        return True\n\n    have_scope = False\n    raw_lines = []\n    with open(resolve_file, 'r') as f:\n        for line in f.readlines():\n            strip_line = line.strip()\n            if not strip_line.startswith('nameserver') or '%' not in strip_line.lstrip('nameserver').strip():\n                raw_lines.append(line)\n                continue\n\n            have_scope = True\n\n    if not have_scope:\n        return True\n\n    action = ui_choice(text('if-remove-ipv6-scope'),[\n        ('y', text('yes')),\n        ('n', text('no')),\n    ])\n    if action.lower() != 'y':\n        return False\n\n    shutil.copyfile(resolve_file, resolve_file+'.bak')\n    with open(resolve_file, 'w') as f:\n        for line in raw_lines:\n            f.write(line)\n\n    return True\n\ndef precheck():\n    if platform.machine() in ('x86_64', 'AMD64') and 'ssse3' not in open('/proc/cpuinfo', 'r').read().lower():\n        log.warning(text('ssse3-not-support'))\n        return False\n\n    if free_memory() < 1024 * 1024 * 1024:\n        log.warning(text('insufficient-memory'))\n        return False\n\n    log.info(text(\"docker-version\"))\n    while True:\n        proc = exec_command('docker', '--version')\n        if proc[0] == 0:\n            t = re.findall(r'^Docker version (\\d+)\\.', proc[1])\n            if len(t) == 0:\n                log.warning(text('docker-not-installed', PRODUCT))\n            elif int(t[0]) < 20:\n                log.warning(text('docker-version-too-low', PRODUCT))\n            else:\n                break\n        else:\n            log.warning(text('docker-not-installed', PRODUCT))\n\n        action = ui_choice(text('if-install-docker'), [\n            ('y', text('yes')),\n            ('n', text('no')),\n        ])\n        if action.lower() == 'n':\n            return False\n        elif action.lower() == 'y':\n            if not install_docker():\n                log.warning(text('install-docker-failed', PRODUCT))\n                return False\n\n    if not precheck_docker_compose():\n        return False\n\n    if not precheck_dns_scope():\n        return False\n\n    return True\n\ndef docker_pull(cwd):\n    log.info(text('docker-pull'))\n    try:\n        subprocess.check_call(compose_command+' pull', cwd=cwd, shell=True)\n        return True\n    except Exception as e:\n        log.warning(\"docker pull error: \"+str(e))\n        return False\n\ndef docker_restart(container):\n    log.info(text('docker-restart')+\": \"+container)\n    try:\n        subprocess.check_call('docker restart '+container, shell=True)\n        return True\n    except Exception as e:\n        log.error(\"docker restart error: \"+str(e))\n        return False\n\ndef docker_exec(container, command):\n    log.info(text('docker-exec')+\": (\"+container+\") \"+command)\n    try:\n        subprocess.check_call('docker exec '+container+' '+command, shell=True)\n        return True\n    except Exception as e:\n        log.error(\"docker exec error: \"+str(e))\n        return False\n\ndef image_clean():\n    log.info(text('image-clean'))\n    proc = exec_command('docker image prune -f --filter=\"label=maintainer=SafeLine-CE\"', shell=True)\n    if proc[0] != 0:\n        log.warning(\"remove docker image failed: \"+proc[2])\n\ndef docker_up(cwd):\n    log.info(text('docker-up'))\n    while True:\n        p = exec_command_with_loading(compose_command+' up -d --remove-orphans', cwd=cwd)\n        if p[0] == 0:\n            return True\n        if 'iptables failed' in p[2]:\n            log.warning(\"docker up error: \"+p[2])\n            while True:\n                action = ui_choice(text('docker-up-iptables-failed'),[\n                    ('y', text('yes')),\n                    ('n', text('no')),\n                ])\n                if action.lower() == 'y':\n                    start_docker()\n                    break\n                elif action.lower() == 'n':\n                    return False\n        else:\n            log.error(\"docker up error: \"+p[2])\n            return False\n\ndef docker_down(cwd):\n    log.info(text('docker-down', PRODUCT))\n    try:\n        subprocess.check_call(compose_command+' down', cwd=cwd, shell=True)\n        return True\n    except Exception:\n        return False\n\ndef get_url_time(url):\n    now = datetime.datetime.now()\n    try:\n        urlopen(url, timeout=10, context=REQUEST_CTX)\n    except HTTPError as e:\n        log.debug(\"get url \"+url+\" status: \"+str(e.status))\n        if e.status > 499:\n            return 999999\n    except Exception as e:\n        log.debug(\"get url \"+url+\" failed: \"+str(e))\n        return 999999\n    return (datetime.datetime.now() - now).microseconds / 1000\n\ndef get_avg_delay(url):\n    log.debug(\"test url avg delay: \"+url)\n    total_delay = 0\n    for i in range(3):\n        total_delay += get_url_time(url)\n\n    avg_delay = total_delay / 3\n    log.debug(\"url \"+url+\" avg delay: \"+str(avg_delay))\n    return avg_delay\n\npull_failed_prefix = []\n\ndef image_source():\n    source = {\n        'https://registry-1.docker.io': 'chaitin',\n        \"https://swr.cn-east-3.myhuaweicloud.com\": 'swr.cn-east-3.myhuaweicloud.com/chaitin-safeline'\n    }\n\n    min_delay = -1\n    image_prefix = ''\n\n    for url, prefix in source.items():\n        if prefix in pull_failed_prefix:\n            continue\n        delay = get_avg_delay(url)\n        if delay > 0 and (min_delay < 0 or delay < min_delay):\n            min_delay = delay\n            image_prefix = prefix\n\n    log.debug(\"use image_prefix: \"+image_prefix)\n    return image_prefix\n\ndef docker_source():\n    sources = [\n        \"https://mirrors.aliyun.com/docker-ce/\",\n        \"https://mirrors.tencent.com/docker-ce/\",\n        \"https://download.docker.com\"\n    ]\n\n    min_delay = -1\n    source = ''\n    for v in sources:\n        delay = get_avg_delay(v)\n        if delay > 0 and (min_delay < 0 or delay < min_delay):\n            min_delay = delay\n            source = v\n    return source\n\ndef read_config(path,config):\n    with open(path, 'r') as f:\n        for line in f.readlines():\n            if line.strip() == '':\n                continue\n            try:\n                s = line.index('=')\n                if s > 0:\n                    k = line[:s].strip()\n                    v = line[s + 1:].strip()\n                    config[k] = v\n            except ValueError:\n                continue\n\ndef write_config(path,config):\n    with open(path, 'w') as f:\n        for k in config:\n            f.write('%s=%s\\n' % (k, config[k]))\n\ndef generate_config(path):\n    log.info(text('update-config'))\n    config = {\n        'SAFELINE_DIR': path,\n        'POSTGRES_PASSWORD': '',\n        'MGT_PORT': '',\n        'RELEASE': '',\n        'CHANNEL': '',\n        'REGION': '',\n        'IMAGE_PREFIX': '',\n        'IMAGE_TAG': '',\n        'SUBNET_PREFIX': '',\n        'ARCH_SUFFIX': ''\n    }\n\n    env_path = os.path.join(path,'.env')\n    if os.path.exists(env_path):\n        read_config(env_path,config)\n\n    if config['ARCH_SUFFIX'] == '':\n        if platform.machine() == 'aarch64':\n            config['ARCH_SUFFIX'] = '-arm'\n\n    if config['POSTGRES_PASSWORD'] == '':\n        config['POSTGRES_PASSWORD'] = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(20)])\n\n    if config['SUBNET_PREFIX'] == '':\n        config['SUBNET_PREFIX'] = rand_subnet()\n\n    if config['RELEASE'] == '' and GLOBAL_ARGS.lts:\n        config['RELEASE'] = '-lts'\n        config['CHANNEL'] = '-lts'\n\n    if config['RELEASE'] == '-lts':\n        GLOBAL_ARGS.lts = True\n\n    default_try = False\n    if config['MGT_PORT'] == '9443':\n        default_try = True\n    while not config['MGT_PORT'].isnumeric() or int(config['MGT_PORT']) >= 65536 or int(config['MGT_PORT']) <= 0 or not check_port(config['MGT_PORT']):\n        if not default_try:\n            config['MGT_PORT'] = '9443'\n            default_try = True\n        else:\n            config['MGT_PORT'] = ui_read(text('input-mgt-port', PRODUCT),None)\n\n    if config['REGION'] == '' and GLOBAL_ARGS.en:\n        config['REGION'] = '-g'\n\n    if not config['POSTGRES_PASSWORD'].isalnum():\n        log.info(text('pg-pass-contains-invalid-char'))\n        raise Exception(text('pg-pass-contains-invalid-char'))\n\n    if config['IMAGE_PREFIX'] == '' or config['IMAGE_PREFIX'] in pull_failed_prefix:\n        config['IMAGE_PREFIX'] = image_source()\n        if config['IMAGE_PREFIX'] == '':\n            raise Exception(text('fail-to-connect-image-source'))\n\n    config['IMAGE_TAG'] = get_version(config['IMAGE_TAG'])\n    log.info(text('target-version', config['IMAGE_TAG']))\n\n    write_config(env_path, config)\n    return config\n\ndef show_address(mgt_port):\n    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.connect((\"8.8.8.8\", 80))\n    local_ip = s.getsockname()[0]\n    log.info(text('go-to-panel', (PRODUCT, local_ip, mgt_port)))\n    log.info(text('go-to-panel', (PRODUCT, '0.0.0.0', mgt_port)))\n\ndef init_mgt():\n    while True:\n        p = exec_command('docker', 'inspect','--format=\\'{{.State.Health.Status}}\\'', 'safeline-mgt')\n        if p[0] == 0 and p[1].strip().replace(\"'\",'') == 'healthy':\n            break\n        elif p[0] != 0:\n            log.debug(\"get safeline-mgt status error: \"+str(p[2]))\n        log.info(text('wait-mgt-health'))\n        time.sleep(5)\n\n    log.info(text('reset-admin'))\n    proc = exec_command('docker exec safeline-mgt /app/mgt-cli reset-admin --once',shell=True)\n    if proc[0] != 0:\n        log.warning(proc[2])\n    elif proc[1].strip() != '':\n        log.info('\\n'+proc[1].strip())\n\ndef check_install_path(safeline_path):\n    if not safeline_path.startswith('/'):\n        return False\n    proc = exec_command('which', 'docker')\n    if proc[0] != 0:\n        log.debug('get docker path failed: '+proc[2])\n        raise Exception(text('fail-to-get-docker-path'))\n    if not proc[1].startswith('/snap'):\n        return True\n    home_path = os.path.expanduser('~')\n    if not safeline_path.startswith(home_path):\n        log.warning(text('snap-docker-should-use-home-path', home_path))\n        return False\n    return True\n\ndef install():\n    global INSTALL\n    INSTALL = True\n    log.info(text('prepare-to-install', PRODUCT))\n\n    if not precheck():\n        log.error(text('precheck-failed', PRODUCT))\n        return\n    log.info(text('precheck-passed'))\n\n    default_path = '/data/safeline'\n    if not SELF:\n        default_path = '/data/waf'\n\n    while True:\n        safeline_path = ui_read(text('input-target-path', PRODUCT), default_path)\n        if not check_install_path(safeline_path):\n            log.warning(text('invalid-path', safeline_path))\n            continue\n        if os.path.exists(safeline_path):\n            log.warning(text('path-exists', safeline_path))\n            continue\n        if free_space(safeline_path) < 5 * 1024 * 1024 * 1024:\n            log.warning(text('insufficient-disk-capacity', (safeline_path, PRODUCT)))\n            continue\n        break\n\n    try:\n        os.makedirs(safeline_path)\n    except Exception as e:\n        log.error(text('fail-to-create-dir', safeline_path) + ' ' + str(e))\n        return\n\n    mgt_path = os.path.join(safeline_path,'resources','mgt')\n    try:\n        os.makedirs(mgt_path, exist_ok=True)\n    except Exception as e:\n        log.error(text('fail-to-create-dir', mgt_path) + ' ' + str(e))\n        return\n    if GLOBAL_ARGS.patch != '':\n        shutil.copyfile(GLOBAL_ARGS.patch, os.path.join(mgt_path, 'product.data'))\n\n    log.info(text('remain-disk-capacity', (safeline_path, humen_size(free_space(safeline_path)))))\n\n    log.info(text('download-compose'))\n    if not save_file_from_url('https://'+DOMAIN+'/release/latest/compose.yaml',os.path.join(safeline_path, 'docker-compose.yaml')):\n        log.error(text('fail-to-download-compose'))\n        return\n    rename_file(os.path.join(safeline_path, 'compose.yaml'),os.path.join(safeline_path, 'compose.yaml.bak'))\n\n    mgt_port = generate_config_and_run(safeline_path)\n    if mgt_port is None:\n        return\n\n    log.info(text('install-finish', PRODUCT))\n    finish(mgt_port)\n\ndef get_installed_dir():\n    safeline_path = ''\n    safeline_path_proc = exec_command('docker','inspect','--format','\\'{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}\\'', 'safeline-mgt')\n    if safeline_path_proc[0] == 0:\n        safeline_path = safeline_path_proc[1].strip().replace(\"'\",'')\n    else:\n        log.debug(\"get installed dir error: \"+ safeline_path_proc[2])\n    log.debug(\"find safeline installed path: \" + safeline_path)\n    if safeline_path == '' or not os.path.exists(safeline_path):\n        log.warning(text('fail-to-get-installed-dir'))\n        return ui_read(text('input-target-path', PRODUCT),None)\n\n    return safeline_path\n\ndef save_file_from_url(url, path):\n    log.debug('saving '+url+' to '+path)\n    data = get_url(url)\n    if data is None:\n        return False\n    with open(path, 'w') as f:\n        f.write(data)\n    return True\n\ndef rename_file(src, dst):\n    if os.path.exists(src):\n        os.rename(src, dst)\n\ndef remove_file(src):\n    if os.path.exists(src):\n        os.remove(src)\n\ndef generate_config_and_run(safeline_path):\n    env_file = os.path.join(safeline_path, '.env')\n    env_bak_file = os.path.join(safeline_path, '.env.bak')\n    if os.path.exists(env_file):\n        shutil.copyfile(env_file, env_bak_file)\n\n    try:\n        while True:\n            config = generate_config(safeline_path)\n            if docker_pull(safeline_path):\n                break\n\n            pull_failed_prefix.append(config['IMAGE_PREFIX'])\n            log.info(text('try-another-image-source'))\n\n        if not docker_up(safeline_path):\n            log.error(text('fail-to-up'))\n            rename_file(env_bak_file, env_file)\n            return None\n    except KeyboardInterrupt:\n        log.warning(text('keyboard-interrupt'))\n        rename_file(env_bak_file, env_file)\n        return None\n    except Exception as e:\n        log.error('start WAF failed: '+str(e))\n        rename_file(env_bak_file, env_file)\n        return None\n\n    remove_file(env_bak_file)\n    return config['MGT_PORT']\n\ndef upgrade():\n    safeline_path = get_installed_dir()\n\n    if not precheck_docker_compose() or not precheck_dns_scope():\n        log.error(text('precheck-failed', PRODUCT))\n        return\n\n    log.info(text('download-compose'))\n    rename_file(os.path.join(safeline_path, 'compose.yaml'), os.path.join(safeline_path, 'compose.yaml.bak'))\n    if not save_file_from_url('https://'+DOMAIN+'/release/latest/compose.yaml', os.path.join(safeline_path, 'docker-compose.yaml')):\n        log.error(text('fail-to-download-compose'))\n        return\n\n    mgt_port = generate_config_and_run(safeline_path)\n    if mgt_port is None:\n        return\n\n    if GLOBAL_ARGS.image_clean:\n        image_clean()\n\n    log.info(text('upgrade-finish', PRODUCT))\n    finish(mgt_port)\n\ndef finish(mgt_port):\n    init_mgt()\n    show_address(mgt_port)\n\ndef reset_tengine():\n    safeline_path = get_installed_dir()\n    resources_path = os.path.join(safeline_path, 'resources')\n    nginx_path = os.path.join(resources_path,'nginx')\n    if not os.path.exists(nginx_path):\n        log.error(text('fail-to-find-nginx'))\n        return\n    backup_path = os.path.join(resources_path, 'nginx.'+str(datetime.datetime.now().timestamp()))\n    log.info(text('nginx-backup-dir') +': '+ backup_path)\n    try:\n        shutil.move(nginx_path, backup_path)\n    except Exception as e:\n        log.error(text('fail-to-backup-nginx')+': '+str(e))\n        return\n\n    if docker_restart('safeline-tengine'):\n        docker_exec('safeline-mgt', 'gentenginewebsite')\n\n    if os.path.exists(os.path.join(backup_path, 'static')):\n        try:\n            shutil.copy(os.path.join(backup_path, 'static'), os.path.join(nginx_path, 'static'))\n        except Exception as e:\n            log.error(text('fail-to-recover-static')+': '+str(e))\n            return\n\n    log.info(text('reset-tengine-finish'))\n\ndef docker_restart_all(cwd):\n    if not docker_down(cwd):\n        log.error(text('fail-to-down'))\n        return False\n\n    if not docker_up(cwd):\n        log.error(text('fail-to-up'))\n        return False\n\n    return True\n\ndef reset_postgres():\n    safeline_path = get_installed_dir()\n\n    env_file = os.path.join(safeline_path, '.env')\n    if not os.path.exists(env_file):\n        log.error(text('fail-to-find-env'))\n        return\n\n    config = {}\n    read_config(env_file, config)\n    config['POSTGRES_PASSWORD'] = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(20)])\n    write_config(env_file, config)\n\n    if not docker_exec('safeline-pg','psql -U safeline-ce -c \"ALTER USER \\\\\"safeline-ce\\\\\" WITH PASSWORD \\''+config['POSTGRES_PASSWORD']+'\\';\"'):\n        log.error(text('fail-to-reset-postgres-password'))\n        return\n\n    action = ui_choice(text('if-restart-docker', PRODUCT), [\n        ('y', text('yes')),\n        ('n', text('no')),\n    ])\n\n    if action.lower() == 'y':\n        if not precheck_docker_compose():\n            log.error(text('precheck-failed', PRODUCT))\n            return\n\n        if not docker_restart_all(safeline_path):\n            return\n\n    log.info(text('reset-postgres-password-finish'))\n\ndef repair():\n    action = ui_choice(text('choice-action'),[\n        ('1', text('reset-tengine')),\n        ('2', text('reset-postgres')),\n    ])\n\n    if action =='1':\n        reset_tengine()\n    elif action =='2':\n        reset_postgres()\n\ndef restart():\n    safeline_path = get_installed_dir()\n\n    if not precheck_docker_compose():\n        log.error(text('precheck-failed', PRODUCT))\n        return\n\n    if not docker_restart_all(safeline_path):\n        return\n\n    log.info(text('restart-docker-finish', PRODUCT))\n\ndef backup():\n    pass\n\ndef uninstall():\n    safeline_path = get_installed_dir()\n\n    action = ui_choice(text('if-remove-waf', (PRODUCT, safeline_path)),[\n        ('y', text('yes')),\n        ('n', text('no')),\n    ])\n\n    if action == 'n':\n        return\n\n    if not precheck_docker_compose():\n        log.error(text('precheck-failed', PRODUCT))\n        return\n\n    if not docker_down(safeline_path):\n        log.error(text('fail-to-docker-down'))\n        return\n\n    image_clean()\n\n    try:\n        shutil.rmtree(safeline_path)\n    except Exception as e:\n        log.debug(\"remove dir failed: \"+str(e))\n        log.error(text('fail-to-remove-dir', PRODUCT))\n\n    log.info(text('uninstall-finish', PRODUCT))\n\nACTION = ''\n\ndef get_version_from_input(old_version):\n    while True:\n        version = ui_read(text('input-target-version', ACTION),None)\n        if not check_version_format(version):\n            log.warning(text('version-format-error', version))\n            continue\n        if not compare_version(old_version, version):\n            log.warning(text('can-not-downgrade', (PRODUCT, old_version, version)))\n            continue\n        return version.lstrip('v')\n\ndef check_version_format(version):\n    if GLOBAL_ARGS.lts and not version.endswith('-lts'):\n        log.debug('lts version should end with -lts')\n        return False\n    split_version = version.lstrip('v').rstrip('-lts').split('.')\n    if len(split_version) != 3:\n        log.debug('split version length is not 3')\n        return False\n    try:\n        for v in split_version:\n            int(v)\n    except ValueError as e:\n        log.debug('check version %s format failed: %s' % (version, str(e)))\n        return False\n    return True\n\ndef compare_version(old_version, new_version):\n    if old_version == 'latest':\n        try:\n            log.info(text('get-version-from-mgt'))\n            old_version = get_version_from_mgt()\n        except Exception as e:\n            log.debug('get version from mgt failed: '+str(e))\n            log.warning(text('fail-to-get-version-from-mgt'))\n            log.warning(text('skip-version-compare', PRODUCT))\n            return True\n    elif old_version == '':\n        return True\n\n    if not check_version_format(old_version):\n        log.warning(text('version-format-error', old_version))\n        log.warning(text('skip-version-compare', PRODUCT))\n        return True\n\n    split_old_version = old_version.lstrip('v').rstrip('-lts').split('.')\n    split_new_version = new_version.lstrip('v').rstrip('-lts').split('.')\n\n    for index in range(len(split_old_version)):\n        int_old_version = int(split_old_version[index])\n        int_new_version = int(split_new_version[index])\n        if int_old_version > int_new_version:\n            return False\n        elif int_old_version < int_new_version:\n            return True\n\n    return True\n\n\ndef get_version_from_mgt():\n    proc = exec_command('docker exec safeline-mgt /app/mgt version',shell=True)\n    if proc[0] != 0:\n        raise Exception('stderr: ' + proc[2])\n    for line in proc[1].split('\\n'):\n        strip_line = line.strip()\n        if not strip_line.startswith('version'):\n            continue\n        return strip_line.lstrip('version').strip()\n\n    raise Exception('mgt version not found')\n\nTARGET_VERSION = ''\n\ndef get_version(old_version):\n    global TARGET_VERSION\n    if TARGET_VERSION != '':\n        return TARGET_VERSION\n\n    log.info(text('get-version', PRODUCT))\n\n    try:\n        data = get_url('https://'+DOMAIN+'/release/latest/version.json')\n        if data is None:\n            TARGET_VERSION = get_version_from_input(old_version)\n            return TARGET_VERSION\n        version = json.loads(data)\n        if GLOBAL_ARGS.lts:\n            latest_version = version['lts_version']\n        else:\n            latest_version = version['latest_version']\n        if not check_version_format(latest_version):\n            log.warning(text('version-format-error', latest_version))\n            TARGET_VERSION = get_version_from_input(old_version)\n        elif not compare_version(old_version, latest_version):\n            log.warning(text('can-not-downgrade', (old_version, latest_version)))\n            TARGET_VERSION = get_version_from_input(old_version)\n        else:\n            TARGET_VERSION = latest_version.lstrip('v')\n        return TARGET_VERSION\n    except Exception as e:\n        log.warning('get version failed: %s' % str(e))\n        TARGET_VERSION = get_version_from_input(old_version)\n        return TARGET_VERSION\n\ndef main():\n    if SELF:\n        banner()\n        log.info(text('hello1'))\n        log.info(text('hello2'))\n        print()\n\n    if GLOBAL_ARGS.lts:\n        log.info(text('install-channel', text('lts-release')))\n\n    if sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor <= 5):\n        log.error(text('python-version-too-low'))\n        return\n\n    if not sys.stdin.isatty():\n        log.error(text('not-a-tty'))\n        return\n\n    if os.geteuid() != 0:\n        log.error(text('not-root'))\n        return\n\n    if platform.system() != 'Linux':\n        log.error(text('not-linux', (PRODUCT, platform.system())))\n        return\n\n    if platform.machine() not in ('aarch64', 'x86_64', 'AMD64'):\n        log.error(text('unsupported-arch', (PRODUCT, platform.machine())))\n        return\n\n\n    action = ui_choice(text('choice-action'), [\n        ('1', text('install')),\n        ('2', text('upgrade')),\n        ('3', text('uninstall')),\n        ('4', text('repair')),\n        ('5', text('restart')),\n        # ('4', text('backup'))\n    ])\n\n    global ACTION\n    if action == '1':\n        ACTION = text('install')\n        install()\n    elif action == '2':\n        ACTION = text('upgrade')\n        upgrade()\n    elif action == '3':\n        uninstall()\n    elif action == '4':\n        repair()\n    elif action == '5':\n        restart()\n    # elif action == '4':\n    #     backup()\n\nif __name__ == '__main__':\n    try:\n        main()\n    except KeyboardInterrupt:\n        log.warning(text('keyboard-interrupt'))\n        pass\n    except Exception as e:\n        log.error(e)\n    finally:\n        if SELF:\n            print(color(text('talking-group') + '\\n', [GREEN]))\n"
  },
  {
    "path": "sdk/ingress-nginx/README.md",
    "content": "# ingress-nginx-safeline\n\n[Ingress-nginx](https://kubernetes.github.io/ingress-nginx/) plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.\n\n## Safeline Prepare\nThe detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.\n\n1.Navigate to the configuration directory of the SafeLine detection engine:\n```shell\ncd /data/safeline/resources/detector/\n```\n2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:\n```yaml\nbind_addr: 0.0.0.0\nlisten_port: 8000\n```\nThese configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.\n\n3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:\n```shell\ncd /data/safeline\n```\n\n4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:\n```yaml\n...\ndetect:\n  ports:\n    - 8000:8000\n...\n```\n\n5.Save the changes and restart SafeLine with the following commands:\n```shell\ndocker-compose down\ndocker-compose up -d\n```\nThis will apply the changes and activate the new configuration.\n\n## Plugin Usage\n\n### Step 1: Install the plugin\n\nway 1: Build your own ingress-nginx/controller image with the plugin installed.\n\n```dockerfile\nFROM registry.k8s.io/ingress-nginx/controller:v1.10.1\n\nUSER root\n\nRUN apk add --no-cache make gcc unzip wget\n\n# install luaroncks\nRUN wget https://luarocks.org/releases/luarocks-3.11.0.tar.gz && \\\n    tar zxpf luarocks-3.11.0.tar.gz && \\\n    cd luarocks-3.11.0 && \\\n    ./configure && \\\n    make && \\\n    make install && \\\n    cd .. && \\\n    rm -rf luarocks-3.11.0 luarocks-3.11.0.tar.gz\n\nRUN luarocks install ingress-nginx-safeline && \\\n    ln -s /usr/local/share/lua/5.1/safeline /etc/nginx/lua/plugins/safeline\n\nUSER www-data\n```\n\nway 2: Use the chaitin ingress-nginx-controller image.\n\nreplace image ingress-nginx-controller with `docker.io/chaitin/ingress-nginx-controller:v1.10.1` in your deployment.\n\n### Step 2: Configure the plugin\n\nuse a ConfigMap to configure the plugin\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: safeline\n  namespace: default\ndata:\n  host: \"YOUR_DETECTOR_HOST\"\n  port: \"YOUR_DETECTOR_PORT\"\n```\n\n### Step 3: Configure the ingress-controller\n\ninject env `SAFELINE_HOST` and `SAFELINE_PORT` to the ingress-controller deployment\n\n```yaml\n...\nenv:\n  - name: SAFELINE_HOST\n    valueFrom:\n      configMapKeyRef:\n        name: safeline\n        key: host\n  - name: SAFELINE_PORT\n    valueFrom:\n      configMapKeyRef:\n        name: safeline\n        key: port\n...\n\n```\n\n### Step 3: Enable the plugin\n\nenable safeline plugin in configmap\n\n```yaml\napiVersion: v1\ndata:\n  allow-snippet-annotations: \"false\"\n  plugins: \"safeline\"\nkind: ConfigMap\nmetadata:\n  name: ingress-nginx-controller\n  namespace: default\n```\n\n### Step 4: Set externalTrafficPolicy to Local\nby default, the ingress-nginx-controller service is of type LoadBalancer, which means the source IP of the request will be the IP of the LoadBalancer. If you want to get the real source IP, you can set the externalTrafficPolicy to Local.\n\n### Step 5: Test the plugin\n\nuse a simple http sql injection test\n\n```bash\ncurl -X POST http://localhost/ -d \"select * from users where id=1 or 1=1\" \n```\n\nyou should get a 403 response.\n```bash\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"b53eb5b95796475699c52a019abb8e6a\"}\n```"
  },
  {
    "path": "sdk/ingress-nginx/ingress-nginx-safeline-1.0.2-1.rockspec",
    "content": "package = \"ingress-nginx-safeline\"\nversion = \"1.0.2-1\"\nsource = {\n   url = \"git://github.com/xbingW/ingress-nginx-safeline.git\"\n}\ndescription = {\n   summary = \"Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/xbingW/ingress-nginx-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n       [\"safeline.main\"] = \"lib/safeline/main.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/ingress-nginx/ingress-nginx-safeline-1.0.3-1.rockspec",
    "content": "package = \"ingress-nginx-safeline\"\nversion = \"1.0.3-1\"\nsource = {\n   url = \"git://github.com/xbingW/ingress-nginx-safeline.git\"\n}\ndescription = {\n   summary = \"Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/xbingW/ingress-nginx-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n       [\"safeline.main\"] = \"lib/safeline/main.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/ingress-nginx/ingress-nginx-safeline-1.0.4-1.rockspec",
    "content": "package = \"ingress-nginx-safeline\"\nversion = \"1.0.4-1\"\nsource = {\n   url = \"git://github.com/chaitin/ingress-nginx-safeline.git\"\n}\ndescription = {\n   summary = \"Ingress-Nginx plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/ingress-nginx-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n       [\"safeline.main\"] = \"lib/safeline/main.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/ingress-nginx/lib/safeline/main.lua",
    "content": "local t1k = require \"resty.t1k\"\nlocal t1k_constants = require \"resty.t1k.constants\"\n\nlocal ngx = ngx\nlocal fmt = string.format\n\nlocal blocked_message = [[{\"code\": %s, \"success\":false, ]] ..\n        [[\"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"%s\"}]]\n\nlocal _M = {}\n\nlocal mode = os.getenv(\"SAFELINE_MODE\")\nlocal host = os.getenv(\"SAFELINE_HOST\")\nlocal port = os.getenv(\"SAFELINE_PORT\")\nlocal connect_timeout = os.getenv(\"SAFELINE_CONNECT_TIMEOUT\")\nlocal send_timeout = os.getenv(\"SAFELINE_SEND_TIMEOUT\")\nlocal read_timeout = os.getenv(\"SAFELINE_READ_TIMEOUT\")\nlocal req_body_size = os.getenv(\"SAFELINE_REQ_BODY_SIZE\")\nlocal keepalive_size = os.getenv(\"SAFELINE_KEEPALIVE_SIZE\")\nlocal keepalive_timeout = os.getenv(\"SAFELINE_KEEPALIVE_TIMEOUT\")\nlocal remote_addr = os.getenv(\"SAFELINE_REMOTE_ADDR\")\n\nlocal function get_conf()\n    local t = {\n        mode = mode or \"block\",\n        host = host,\n        port = port,\n        connect_timeout = connect_timeout or 1000,\n        send_timeout = send_timeout or 1000,\n        read_timeout = read_timeout or 1000,\n        req_body_size = req_body_size or 1024,\n        keepalive_size = keepalive_size or 256,\n        keepalive_timeout = keepalive_timeout or 60000,\n        remote_addr = remote_addr\n    }\n    return t\nend\n\nfunction _M.rewrite()\n    local t = get_conf()\n    if not t.host then\n        ngx.log(ngx.ERR, \"safeline host is required\")\n        return\n    end\n    local ok, err, result = t1k.do_access(t, false)\n    if not ok then\n        ngx.log(ngx.ERR, \"failed to detector req: \", err)\n        return\n    end\n    if result then\n        if result.action == t1k_constants.ACTION_BLOCKED then\n            local msg = fmt(blocked_message, result.status, result.event_id)\n            ngx.log(ngx.ERR, \"blocked by safeline waf: \",msg)\n            ngx.status = tonumber(result.status,10)\n            ngx.say(msg)\n            return ngx.exit(ngx.HTTP_OK)\n        end\n    end\nend\n\nreturn _M"
  },
  {
    "path": "sdk/kong/Readme.md",
    "content": "# Kong Safeline Plugin\nKong plugin for Chaitin SafeLine Web Application Firewall (WAF). This plugin is used to protect your API from malicious requests. It can be used to block requests that contain malicious content in the request body, query parameters, headers, or URI.\n\n## Safeline Prepare\nThe detection engine of the SafeLine provides services by default via Unix socket. We need to modify it to use TCP, so it can be called by the t1k plugin.\n\n1.Navigate to the configuration directory of the SafeLine detection engine:\n```shell\ncd /data/safeline/resources/detector/\n```\n2.Open the `detector.yml` file in a text editor. Modify the bind configuration from Unix socket to TCP by adding the following settings:\n```yaml\nbind_addr: 0.0.0.0\nlisten_port: 8000\n```\nThese configuration values will override the default settings in the container, making the SafeLine engine listen on port 8000.\n\n3.Next, map the container’s port 8000 to the host machine. First, navigate to the SafeLine installation directory:\n```shell\ncd /data/safeline\n```\n\n4.Open the compose.yaml file in a text editor and add the ports field to the detector container to expose port 8000:\n```yaml\n...\ndetect:\n  ports:\n    - 8000:8000\n...\n```\n\n5.Save the changes and restart SafeLine with the following commands:\n```shell\ndocker-compose down\ndocker-compose up -d\n```\nThis will apply the changes and activate the new configuration.\n\n## Plugin Installation\nTo install the plugin, run the following command in your Kong server:\n\n```shell\n$ luarocks install kong-safeline\n```\n\n## Plugin Configuration\nYou can add the plugin to your API by making the following request:\n\n```shell\n# if your detector is running on tcp port\n$ curl -X POST http://kong:8001/services/{name}/plugins \\\n    --data \"name=safeline\" \\\n    --data \"config.host=your_detector_host\" \\\n    --data \"config.port=your_detector_port\"\n# if your detector is running on unix socket\n$ curl -X POST http://kong:8001/services/{name}/plugins \\\n    --data \"name=safeline\" \\\n    --data \"config.host=unix:/path/to/your/unix/socket\"\n```\n\n## Test\nYou can test the plugin by sending a request to your API with malicious content. If the request is blocked, you will receive a `403 Forbidden` response.\n\n```shell\n$ curl -X POST http://kong:8000?1=1%20and%202=2\n\n# you will receive a 403 Forbidden response\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"8b41a021ea9541c89bb88f3773b4da24\"}\n```\n"
  },
  {
    "path": "sdk/kong/kong/plugins/safeline/handler.lua",
    "content": "local t1k = require \"resty.t1k\"\nlocal t1k_constants = require \"resty.t1k.constants\"\n\nlocal fmt = string.format\n\nlocal SafelineHandler = {\n    VERSION = \"0.0.1\",\n    PRIORITY = 1000\n}\n\nlocal blocked_message = [[{\"code\": %s, \"success\":false, ]] ..\n                            [[\"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"%s\"}]]\n\nlocal function get_conf(conf)\n    local t = {\n        mode = conf.mode,\n        host = conf.host,\n        port = conf.port,\n        connect_timeout = conf.connect_timeout,\n        send_timeout = conf.send_timeout,\n        read_timeout = conf.read_timeout,\n        req_body_size = conf.req_body_size,\n        keepalive_size = conf.keepalive_size,\n        keepalive_timeout = conf.keepalive_timeout,\n        remote_addr = conf.remote_addr\n    }\n    return t\nend\n\nfunction SafelineHandler:access(conf)\n    -- your custom code here\n    local t = get_conf(conf)\n    local ok, err, result = t1k.do_access(t, false)\n    if not ok then\n        kong.log.err(\"failed to detector req: \", err)\n    end\n    if result and result.status then\n        if result.action == t1k_constants.ACTION_BLOCKED then\n            local msg = fmt(blocked_message, result.status, result.event_id)\n            kong.log.debug(\"blocked by safeline: \",msg)\n            return kong.response.exit(tonumber(result.status), msg)\n        end\n    end\nend\n\n\nreturn SafelineHandler\n"
  },
  {
    "path": "sdk/kong/kong/plugins/safeline/schema.lua",
    "content": "local typedefs = require \"kong.db.schema.typedefs\"\n\nreturn {\n    name = \"kong-safeline\",\n    fields = {{\n        consumer = typedefs.no_consumer\n    }, {\n        protocols = typedefs.protocols_http\n    }, {\n        config = {\n            type = \"record\",\n            fields = {{\n                host = {\n                    type = \"string\",\n                    required = true\n                }\n            }, {\n                port = {\n                    type = \"number\",\n                    required = false\n                }\n            }, {\n                mode = {\n                    type = \"string\",\n                    required = false,\n                    default = \"block\",\n                    one_of = {\"monitor\", \"block\", \"off\"}\n                }\n            }, {\n                connect_timeout = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                send_timeout = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                read_timeout = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                req_body_size = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                keepalive_size = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                keepalive_timeout = {\n                    type = \"number\",\n                    required = false,\n                    default = 1000\n                }\n            }, {\n                remote_addr = {\n                    type = \"string\",\n                    required = false\n                }\n            }}\n        }\n    }}\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.0-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.0-1\"\nsource = {\n   url = \"git://github.com/xbingW/kong-safeline.git\"\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/xbingW/kong-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.1-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.1-1\"\nsource = {\n   url = \"git://github.com/xbingW/kong-safeline.git\"\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/xbingW/kong-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.2-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.2-1\"\nsource = {\n   url = \"git://github.com/xbingW/kong-safeline.git\"\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/xbingW/kong-safeline\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.3-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.3-1\"\nsource = {\n   url = \"file://kong-safeline-1.0.3.tar.gz\",\n}\nbuild = {\n  type = \"script\",\n  rockspec = {\n    build = {\n      \"git clone https://github.com/chaitin/SafeLine.git\",\n      \"cp -r sdk/kong .\",\n      \"rm -rf SafeLine\"\n    }\n  }\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/SafeLine\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.4-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.4-1\"\nsource = {\n   url = \"git://github.com/chaitin/SafeLine.git\",\n}\nbuild = {\n  type = \"script\",\n  rockspec = {\n    build = {\n      \"git clone https://github.com/chaitin/SafeLine.git\",\n      \"cp -r sdk/kong .\",\n      \"rm -rf SafeLine\"\n    }\n  }\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/SafeLine\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.5-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.5-1\"\nsource = {\n   url = \"git://github.com/chaitin/SafeLine.git\",\n}\nbuild = {\n  type = \"script\",\n  rockspec = {\n    build = {\n      \"git clone https://github.com/chaitin/SafeLine.git\",\n      \"cp -r sdk/kong .\",\n      \"rm -rf SafeLine\"\n    }\n  }\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/SafeLine\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.6-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.6-1\"\nsource = {\n   url = \"git://github.com/xbingW/kong-safeline.git\",\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/SafeLine\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/kong/kong-safeline-1.0.7-1.rockspec",
    "content": "package = \"kong-safeline\"\nversion = \"1.0.7-1\"\nsource = {\n   url = \"git://github.com/chaitin/kong-safeline.git\",\n}\ndescription = {\n   summary = \"Kong plugin for Chaitin SafeLine Web Application Firewall\",\n   homepage = \"https://github.com/chaitin/SafeLine\",\n   license = \"Apache License 2.0\",\n   maintainer = \"Xiaobing Wang <xiaobing.wang@chaitin.com>\"\n}\ndependencies = {\n   \"lua-resty-t1k >= 1.1.5\"\n}\nbuild = {\n   type = \"builtin\",\n   modules = {\n      [\"kong.plugins.safeline.handler\"] = \"kong/plugins/safeline/handler.lua\",\n      [\"kong.plugins.safeline.schema\"] = \"kong/plugins/safeline/schema.lua\"\n   }\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/.github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Install Lua\n        uses: leafo/gh-actions-lua@v10\n\n      - name: Install Luarocks\n        uses: leafo/gh-actions-luarocks@v4\n\n      - name: Extract release tag\n        id: release_tag\n        run: |\n          # Extract the tag name from the ref\n          tag=\"${GITHUB_REF#refs/tags/}\"\n          version_without_v=\"${tag#v}\"\n          echo \"version=${tag}\" >> $GITHUB_ENV\n          echo \"version_without_v=${version_without_v}\" >> $GITHUB_ENV\n\n      - name: Create Release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ env.version }}\n          release_name: ${{ env.version }}\n          draft: false\n          prerelease: false\n\n      - name: Upload to luarocks\n        env:\n          LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }}\n        run: |\n          luarocks install dkjson\n          luarocks upload rockspec/lua-resty-t1k-${{ env.version_without_v }}-0.rockspec --api-key=${{ secrets.LUAROCKS_API_KEY }}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/.github/workflows/test.yml",
    "content": "name: Test\n\non: [ push, pull_request ]\n\njobs:\n  luacheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: leafo/gh-actions-lua@v10\n        with:\n          luaVersion: \"luajit-openresty\"\n      - uses: leafo/gh-actions-luarocks@v4\n      - run: luarocks install luacheck\n      - run: luacheck lib\n\n  run_tests:\n    strategy:\n      matrix:\n        openresty_version:\n          - 1.17.8.2\n          - 1.19.9.1\n          - 1.21.4.3\n          - 1.25.3.1\n\n    services:\n      detector:\n        image: chaitin/safeline-detector:t1k-ci-1.6.0\n\n    runs-on: ubuntu-latest\n    container:\n      image: openresty/openresty:${{ matrix.openresty_version }}-alpine-fat\n      # --init runs tinit as PID 1 and prevents the 'WARNING: killing the child process' spam from the test suite\n      options: --init\n\n    steps:\n      - name: Install deps\n        run: |\n          apk add --no-cache bash bind-tools curl git git-lfs libarchive-tools perl perl-dev wget\n          ln -s /usr/bin/bsdtar /usr/bin/tar\n\n      - name: Install CPAN\n        run: curl -s -L http://xrl.us/cpanm > /bin/cpanm && chmod +x /bin/cpanm\n\n      - name: Cache\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cpan\n            ~/.cache\n          key: ${{ runner.os }}-${{ matrix.openresty_version }}-cache\n\n      - name: Install Test::Nginx\n        run: cpanm -q -n Test::Nginx\n\n      - uses: actions/checkout@v4\n        with:\n          lfs: true\n\n      - name: Run tests\n        run: |\n          curl -fs -X POST -H \"Content-Type: application/octet-stream\" --data-binary \"@ci/bytecode\" \"http://detector:8001/update/policy\"\n          env DETECTOR_IP=$(dig detector +short) prove -r t/\n"
  },
  {
    "path": "sdk/lua-resty-t1k/.gitignore",
    "content": "# IntelliJ project files\n.idea\n*.iml\nout\ngen\n\n# Visual Studio Code\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Local History for Visual Studio Code\n.history/\n\n# Built Visual Studio Code Extensions\n*.vsix\n\n# Test::Nginx files\nt/servroot\n\n# luarocks build files\n*.src.rock\n"
  },
  {
    "path": "sdk/lua-resty-t1k/.luacheckrc",
    "content": "std = \"ngx_lua\"\nredefined = false\nmax_line_length = 130\nmax_code_line_length = 130\nmax_string_line_length = 130\nmax_comment_line_length\t= 130\n"
  },
  {
    "path": "sdk/lua-resty-t1k/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 2023 Beijing Chaitin Technology Co., Ltd.\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": "sdk/lua-resty-t1k/README.md",
    "content": "# lua-resty-t1k\n\n[![LuaRocks](https://img.shields.io/luarocks/v/blaisewang/lua-resty-t1k?style=flat-square)](https://luarocks.org/modules/blaisewang/lua-resty-t1k)\n[![Releases](https://img.shields.io/github/v/release/chaitin/lua-resty-t1k?style=flat-square)](https://github.com/chaitin/lua-resty-t1k/releases)\n[![License](https://img.shields.io/github/license/chaitin/lua-resty-t1k?color=ff69b4&style=flat-square)](https://github.com/chaitin/lua-resty-t1k/blob/main/LICENSE)\n\n## Name\n\nLua implementation of the T1K protocol for [Chaitin/SafeLine](https://github.com/chaitin/safeline) Web Application Firewall.\n\n## Status\n\nProduction ready.\n\n[![Test](https://img.shields.io/github/actions/workflow/status/chaitin/lua-resty-t1k/test.yml?logo=github&style=flat-square)](https://github.com/chaitin/lua-resty-t1k/actions)\n\n## Installation\n\n```bash\nluarocks install lua-resty-t1k\n```\n\nIf you are in Mainland China\n\n```bash\nluarocks install lua-resty-t1k --server https://luarocks.cn\n```\n\n## Synopsis\n\n```lua\nlocation / {\n    access_by_lua_block {\n        local t1k = require \"resty.t1k\"\n\n        local t = {\n            mode = \"block\",                            -- block or monitor or off, default off\n            host = \"unix:/workdir/snserver.sock\",      -- required, SafeLine WAF detection service host, unix domain socket, IP, or domain is supported, string\n            port = 8000,                               -- required when the host is an IP or domain, SafeLine WAF detection service port, integer\n            connect_timeout = 1000,                    -- connect timeout, in milliseconds, integer, default 1s (1000ms)\n            send_timeout = 1000,                       -- send timeout, in milliseconds, integer, default 1s (1000ms)\n            read_timeout = 1000,                       -- read timeout, in milliseconds, integer, default 1s (1000ms)\n            req_body_size = 1024,                      -- request body size, in KB, integer, default 1MB (1024KB)\n            keepalive_size = 256,                      -- maximum concurrent idle connections to the SafeLine WAF detection service, integer, default 256\n            keepalive_timeout = 60000,                 -- idle connection timeout, in milliseconds, integer, default 60s (60000ms)\n            remote_addr = \"http_x_forwarded_for: 1\",   -- remote address from ngx.var.VARIABLE, string, default from ngx.var.remote_addr\n        }\n\n        local ok, err, _ = t1k.do_access(t, true)\n        if not ok then \n            ngx.log(ngx.ERR, err)\n        end\n    }\n\n    header_filter_by_lua_block {\n        local t1k = require \"resty.t1k\"\n        t1k.do_header_filter()\n    }\n}\n```\n\n## Lua Resty T1K vs. C T1K\n\n[C T1K](https://t1k.chaitin.com/), as part of SafeLine's enterprise edition, is a deployment mode crafted in C language for enhanced performance.\nIt is compatible with all versions of Nginx and does not require deployment via OpenResty (lua_nginx_module).\n\n|                       | Lua Resty T1K | C T1K |\n|-----------------------|---------------|-------|\n| Request Detection     | ✅             | ✅     |\n| Response Detection    | ❌             | ✅     |\n| Health Checks*        | ❌             | ✅     |\n| Cookie Protection     | ❌             | ✅     |\n| Bot Protection        | ❌             | ✅     |\n| Proxy-side Statistics | ❌             | ✅     |\n\n&ast; APISIX implements health check functionality for the `chaitin-waf` plugin. For more information, please see the [chaitin-waf documentation](https://apisix.apache.org/docs/apisix/next/plugins/chaitin-waf/).\n"
  },
  {
    "path": "sdk/lua-resty-t1k/ci/.dockerignore",
    "content": "/bytecode\n"
  },
  {
    "path": "sdk/lua-resty-t1k/ci/Dockerfile",
    "content": "# Usage:\n#   docker build -t chaitin/safeline-detector:t1k-ci-1.6.0 .\n#   docker push chaitin/safeline-detector:t1k-ci-1.6.0\nFROM chaitin/safeline-detector:1.6.0\n\nRUN sed -i \"s/^# bind_addr/bind_addr/; s/^# listen_port/listen_port/; s/^bind_addr: unix/# bind_addr: unix/;\" /detector/snserver.yml\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/buffer.lua",
    "content": "local _M = {\n    _VERSION = '1.0.0',\n}\n\nfunction _M:new(o)\n    o = o or {}\n    setmetatable(o, self)\n    self.__index = self\n    return o\nend\n\nfunction _M:add(v)\n    self[#self + 1] = v\nend\n\nfunction _M:len()\n    local len = 0\n    for _, v in ipairs(self) do\n        len = len + #v\n    end\n    return len\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/constants.lua",
    "content": "local t = {}\n\nt.ACTION_PASSED = \".\"\nt.ACTION_BLOCKED = \"?\"\n\nt.MODE_OFF = \"off\"\nt.MODE_BLOCK = \"block\"\nt.MODE_MONITOR = \"monitor\"\n\nt.T1K_HEADER_SIZE = 5\n\nt.TAG_HEAD = 0x01\nt.TAG_BODY = 0x02\nt.TAG_EXTRA = 0x03\nt.TAG_VERSION = 0x20\nt.TAG_EXTRA_HEADER = 0x23\nt.TAG_EXTRA_BODY = 0x24\n\nt.MASK_FIRST = 0x40\nt.MASK_LAST = 0x80\n\nt.NGX_HTTP_HEADER_PREFIX = \"http_\"\n\nt.BLOCK_CONTENT_TYPE = \"application/json\"\nt.BLOCK_CONTENT_FORMAT = [[\n{\"code\": %s, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"%s\"}]]\n\nt.UNIX_SOCK_PREFIX = \"unix:\"\n\nreturn t\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/file.lua",
    "content": "local buffer = require \"resty.t1k.buffer\"\n\nlocal _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal buffer_size = 2 ^ 13\n\nfunction _M.read(p, size)\n    size = (not size or size < 0) and 0 or size\n\n    local f, err = io.open(p, \"rb\")\n    if not f or err then\n        return nil, err, nil\n    end\n\n    local left = size\n    local buf = buffer:new()\n\n    while left ~= 0 do\n        local block_size = math.min(left, buffer_size)\n        local block = f:read(block_size)\n        if not block then\n            break\n        end\n        buf:add(block)\n        left = math.max(left - block_size, 0)\n    end\n    f:close()\n\n    return true, nil, buf\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/filter.lua",
    "content": "local _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal find = string.find\nlocal sub = string.sub\n\nlocal ngx = ngx\n\nlocal function parse_extra_header(extra_header)\n    local t = {}\n    local idx = 1\n\n    while (idx <= #extra_header) do\n        local key, val\n        local _, to = find(extra_header, \":\", idx)\n        if to == 0 then\n            break\n        else\n            key = sub(extra_header, idx, to - 1)\n        end\n\n        idx = to + 1\n        _, to = find(extra_header, \"\\n\", idx)\n        if to == 0 then\n            break\n        else\n            val = sub(extra_header, idx, to - 1)\n        end\n\n        t[key] = val\n        idx = to + 1\n    end\n\n    return t\nend\n\nfunction _M.do_header_filter()\n    local extra_header = ngx.ctx.t1k_extra_header\n    if extra_header ~= nil then\n        local header_table = parse_extra_header(extra_header)\n        for k, v in pairs(header_table) do\n            if k ~= nil and v ~= nil then\n                ngx.header[k] = v\n            end\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/handler.lua",
    "content": "local consts = require \"resty.t1k.constants\"\nlocal log = require \"resty.t1k.log\"\n\nlocal fmt = string.format\n\nlocal ngx = ngx\n\nlocal log_fmt = log.fmt\n\nlocal _M = {\n    _VERSION = '1.0.0'\n}\n\nfunction _M.handle(t)\n    local t_type = type(t)\n    if t_type ~= \"table\" then\n        local err = log_fmt(\"invalid result type: %s\", t_type)\n        return nil, err\n    end\n\n    local action = t[\"action\"]\n    if action == consts.ACTION_PASSED then\n        return true, nil\n    elseif action == consts.ACTION_BLOCKED then\n        ngx.status = t[\"status\"] or ngx.HTTP_FORBIDDEN\n        ngx.header.content_type = consts.BLOCK_CONTENT_TYPE\n        ngx.say(fmt(consts.BLOCK_CONTENT_FORMAT, ngx.status, t[\"event_id\"]))\n\n        return ngx.exit(ngx.status)\n    else\n        local err = log_fmt(\"unknown action from t1k server: %s\", action)\n        return nil, err\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/log.lua",
    "content": "local _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal fmt = string.format\n\nlocal ERR = ngx.ERR\nlocal WARN = ngx.WARN\nlocal DEBUG = ngx.DEBUG\n\nfunction _M.fmt(formatstring, ...)\n    return fmt(\"lua-resty-t1k: \" .. formatstring, ...)\nend\n\nfunction _M.err_fmt(formatstring, ...)\n    return ERR, _M.fmt(formatstring, ...)\nend\n\nfunction _M.warn_fmt(formatstring, ...)\n    return WARN, _M.fmt(formatstring, ...)\nend\n\nfunction _M.debug_fmt(formatstring, ...)\n    return DEBUG, _M.fmt(formatstring, ...)\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/request.lua",
    "content": "local bit = require \"bit\"\n\nlocal buffer = require \"resty.t1k.buffer\"\nlocal consts = require \"resty.t1k.constants\"\nlocal file = require \"resty.t1k.file\"\nlocal log = require \"resty.t1k.log\"\nlocal utils = require \"resty.t1k.utils\"\nlocal uuid = require \"resty.t1k.uuid\"\n\nlocal _M = {\n    _VERSION = '1.0.0',\n}\n\nlocal bor = bit.bor\nlocal byte = string.byte\nlocal char = string.char\nlocal fmt = string.format\nlocal sub = string.sub\nlocal concat = table.concat\n\nlocal ngx = ngx\nlocal nlog = ngx.log\nlocal ngx_req = ngx.req\nlocal ngx_socket = ngx.socket\nlocal ngx_var = ngx.var\n\nlocal warn_fmt = log.warn_fmt\nlocal debug_fmt = log.debug_fmt\n\nlocal KEY_EXTRA_UUID = \"UUID\"\nlocal KEY_EXTRA_LOCAL_ADDR = \"LocalAddr\"\nlocal KEY_EXTRA_LOCAL_PORT = \"LocalPort\"\nlocal KEY_EXTRA_REMOTE_ADDR = \"RemoteAddr\"\nlocal KEY_EXTRA_REMOTE_PORT = \"RemotePort\"\nlocal KEY_EXTRA_SCHEME = \"Scheme\"\nlocal KEY_EXTRA_SERVER_NAME = \"ServerName\"\nlocal KEY_EXTRA_PROXY_NAME = \"ProxyName\"\nlocal KEY_EXTRA_REQ_BEGIN_TIME = \"ReqBeginTime\"\nlocal KEY_EXTRA_HAS_RSP_IF_OK = \"HasRspIfOK\"\nlocal KEY_EXTRA_HAS_RSP_IF_BLOCK = \"HasRspIfBlock\"\n\nlocal TAG_HEAD_WITH_MASK_FIRST = bor(consts.TAG_HEAD, consts.MASK_FIRST)\nlocal TAG_EXTRA_WITH_MASK_LAST = bor(consts.TAG_EXTRA, consts.MASK_LAST)\n\nlocal T1K_PROTO = \"Proto:2\\n\"\nlocal T1K_PROTO_DATA = fmt(\"%s%s%s\", char(consts.TAG_VERSION), utils.int_to_char_length(#T1K_PROTO), T1K_PROTO)\n\nlocal function read_request_body(opt_req_body_size)\n    local ok, err\n    local req_body, req_body_size\n\n    local content_length = tonumber(ngx_var.http_content_length) or 0\n    local transfer_encoding = ngx_var.http_transfer_encoding\n    if content_length == 0 and not transfer_encoding then\n        return true, nil, nil\n    end\n\n    ngx_req.read_body()\n    req_body = ngx_req.get_body_data()\n    if req_body then\n        req_body_size = #req_body\n        if req_body_size > opt_req_body_size then\n            nlog(debug_fmt(\"request body is too long: %d bytes, cut to %d bytes\", req_body_size, opt_req_body_size))\n            req_body = sub(req_body, 1, opt_req_body_size)\n        end\n\n        return true, nil, req_body\n    end\n\n    local path = ngx_req.get_body_file()\n    if not path then\n        return true, nil, nil\n    end\n\n    ok, err, req_body = file.read(path, opt_req_body_size)\n    if not ok then\n        err = fmt(\"failed to read temporary file %s: %s\", path, err)\n        return ok, err, nil\n    end\n\n    return true, nil, req_body\nend\n\nlocal function get_remote_addr(remote_addr_var, remote_addr_idx)\n    local addr\n    if remote_addr_var then\n        addr = utils.get_indexed_element(ngx_var[remote_addr_var], remote_addr_idx)\n    end\n    return addr or ngx_var.remote_addr\nend\n\nlocal function parse_v(v)\n    if type(v) == \"table\" then\n        return concat(v, \", \")\n    end\n    return tostring(v)\nend\n\nlocal function build_header()\n    local http_version = ngx_req.http_version()\n    if http_version < 2.0 then\n        return true, nil, ngx_req.raw_header()\n    end\n\n    local headers, err = ngx_req.get_headers(0, true)\n    if err then\n        err = fmt(\"failed to call ngx_req.get_headers: %s\", err)\n        return nil, err, nil\n    end\n\n    local buf = buffer:new()\n    buf:add(fmt(\"%s %s HTTP/%.1f\\r\\n\", ngx_req.get_method(), ngx_var.request_uri, http_version))\n\n    for k, v in pairs(headers) do\n        buf:add(fmt(\"%s: %s\\r\\n\", k, parse_v(v)))\n    end\n    buf:add(\"\\r\\n\")\n\n    return true, nil, buf\nend\n\nlocal function build_body(opts)\n    local ok, err\n    local body\n\n    local req_body_size = opts.req_body_size * 1024\n    ok, err, body = read_request_body(req_body_size)\n    if not ok then\n        return ok, err, nil\n    end\n\n    return true, nil, body\nend\n\nlocal function build_extra(opts)\n    local err\n\n    local src_ip = get_remote_addr(opts.remote_addr_var, opts.remote_addr_idx)\n    if not src_ip then\n        err = fmt(\"failed to get remote_addr, var: %s, idx %d\", opts.remote_addr_var, opts.remote_addr_idx)\n        return nil, err\n    end\n\n    local src_port = ngx_var.remote_port\n    if not src_port then\n        err = \"failed to get ngx_var.remote_port\"\n        return nil, err, nil\n    end\n\n    local local_ip = ngx_var.server_addr\n    if not local_ip then\n        err = \"failed to get ngx_var.server_addr\"\n        return nil, err, nil\n    end\n\n    local local_port = ngx_var.server_port\n    if not local_port then\n        err = \"failed to get ngx_var.server_port\"\n        return nil, err, nil\n    end\n\n    local extra = buffer:new({\n        KEY_EXTRA_UUID, \":\", uuid.generate_v4(), \"\\n\",\n        KEY_EXTRA_REMOTE_ADDR, \":\", src_ip, \"\\n\",\n        KEY_EXTRA_REMOTE_PORT, \":\", src_port, \"\\n\",\n        KEY_EXTRA_LOCAL_ADDR, \":\", local_ip, \"\\n\",\n        KEY_EXTRA_LOCAL_PORT, \":\", local_port, \"\\n\",\n        KEY_EXTRA_SCHEME, \":\", ngx_var.scheme, \"\\n\",\n        KEY_EXTRA_SERVER_NAME, \":\", ngx_var.server_name, \"\\n\",\n        KEY_EXTRA_PROXY_NAME, \":\", ngx_var.hostname, \"\\n\",\n        KEY_EXTRA_REQ_BEGIN_TIME, \":\", fmt(\"%.0f\", ngx_req.start_time() * 1000000), \"\\n\",\n        KEY_EXTRA_HAS_RSP_IF_OK, \":n\\n\",\n        KEY_EXTRA_HAS_RSP_IF_BLOCK, \":n\\n\"\n    })\n\n    return true, nil, extra\nend\n\nlocal function do_send(sock, data)\n    local ok, err = sock:send(data)\n    if not ok then\n        return ok, err\n    end\n    return true, nil\nend\n\nlocal function receive_data(s, srv)\n    local t = {}\n    local ft = true\n    local finished\n\n    repeat\n        local err\n        local tag, length, packet, rsp_body\n\n        packet, err = s:receive(consts.T1K_HEADER_SIZE)\n        if err then\n            err = fmt(\"failed to receive info packet from t1k server %s: %s\", srv, err)\n            return nil, err, nil\n        end\n        if not packet then\n            err = fmt(\"empty packet from t1k server %s\", srv)\n            return nil, err, nil\n        end\n\n        if ft then\n            if not utils.is_mask_first(byte(packet, 1, 1)) then\n                err = fmt(\"first packet is not MASK_FIRST from t1k server %s\", srv)\n                return nil, err, nil\n            end\n            ft = false\n        end\n\n        finished, tag, length = utils.packet_parser(packet)\n        if length > 0 then\n            rsp_body, err = s:receive(length)\n            if not rsp_body or #rsp_body ~= length then\n                err = fmt(\"failed to receive payload from t1k server %s: %s\", srv, err)\n                return nil, err, nil\n            end\n            t[tag] = rsp_body\n        end\n\n    until (finished)\n\n    return true, nil, t\nend\n\nlocal function get_socket(opts)\n    local ok, err\n    local count, sock, server\n\n    sock, err = ngx_socket.tcp()\n    if not sock then\n        err = fmt(\"failed to create socket: %s\", err)\n        return nil, err, nil, nil\n    end\n\n    sock:settimeouts(opts.connect_timeout, opts.send_timeout, opts.read_timeout)\n\n    if opts.uds then\n        server = opts.host\n        ok, err = sock:connect(opts.host)\n    else\n        server = fmt(\"%s:%d\", opts.host, opts.port)\n        ok, err = sock:connect(opts.host, opts.port)\n    end\n    if not ok then\n        sock:close()\n        err = fmt(\"failed to connect to t1k server %s: %s\", server, err)\n        return ok, err, nil, nil\n    end\n    nlog(debug_fmt(\"successfully connected to t1k server %s\", server))\n\n    count, err = sock:getreusedtimes()\n    if not count then\n        nlog(warn_fmt(\"failed to get reused times from t1k server %s: %s\", server, err))\n    end\n\n    if count == 0 then\n        ok, err = sock:setoption(\"keepalive\", true)\n        if not ok then\n            nlog(warn_fmt(\"failed to set keepalive for t1k server %s: %s\", server, err))\n        end\n        ok, err = sock:setoption(\"reuseaddr\", true)\n        if not ok then\n            nlog(warn_fmt(\"failed to set reuseaddr for t1k server %s: %s\", server, err))\n        end\n        ok, err = sock:setoption(\"tcp-nodelay\", true)\n        if not ok then\n            nlog(warn_fmt(\"failed to set tcp-nodelay for t1k server %s: %s\", server, err))\n        end\n    end\n\n    return true, nil, sock, server\nend\n\nlocal function do_socket(opts, header, body, extra)\n    local ok, err\n    local t, sock, server\n\n    ok, err, sock, server = get_socket(opts)\n    if not ok then\n        err = fmt(\"failed to get socket: %s\", err)\n        return ok, err, nil\n    end\n\n    ok, err = do_send(sock, { char(TAG_HEAD_WITH_MASK_FIRST), utils.int_to_char_length(header:len()), header })\n    if not ok then\n        sock:close()\n        err = fmt(\"failed to send header data to t1k server %s: %s\", server, err)\n        return ok, err, nil\n    end\n\n    if body ~= nil then\n        ok, err = do_send(sock, { char(consts.TAG_BODY), utils.int_to_char_length(body:len()), body })\n        if not ok then\n            sock:close()\n            err = fmt(\"failed to send body data to t1k server %s: %s\", server, err)\n            return ok, err, nil\n        end\n    end\n\n    ok, err = do_send(sock, { T1K_PROTO_DATA, char(TAG_EXTRA_WITH_MASK_LAST), utils.int_to_char_length(extra:len()), extra })\n    if not ok then\n        sock:close()\n        err = fmt(\"failed to send extra data to t1k server %s: %s\", server, err)\n        return ok, err, nil\n    end\n\n    ok, err, t = receive_data(sock, server)\n    if not ok then\n        return ok, err, nil\n    end\n\n    ok, err = sock:setkeepalive(opts.keepalive_timeout, opts.keepalive_size)\n    if not ok then\n        nlog(warn_fmt(\"failed to set keepalive: %s\", err))\n        sock:close()\n    end\n\n    return true, nil, t\nend\n\nfunction _M.do_request(opts)\n    local ok, err\n    local header, body, extra, t\n\n    ok, err, header = build_header(opts)\n    if not ok then\n        return ok, err, nil\n    end\n\n    ok, err, body = build_body(opts)\n    if not ok then\n        return ok, err, nil\n    end\n\n    ok, err, extra = build_extra(opts)\n    if not ok then\n        return ok, err, nil\n    end\n\n    ok, err, t = do_socket(opts, header, body, extra)\n    if not ok then\n        return ok, err, nil\n    end\n\n    if opts.mode == consts.MODE_BLOCK then\n        local extra_header = t[consts.TAG_EXTRA_HEADER]\n        if extra_header then\n            ngx.ctx.t1k_extra_header = extra_header\n        end\n    end\n\n    local result = {\n        action = t[consts.TAG_HEAD],\n        status = t[consts.TAG_BODY],\n        event_id = utils.get_event_id(t[consts.TAG_EXTRA_BODY]),\n    }\n\n    return true, nil, result\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/utils.lua",
    "content": "local consts = require \"resty.t1k.constants\"\n\nlocal _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal band = bit.band\nlocal bnot = bit.bnot\nlocal lshift = bit.lshift\nlocal rshift = bit.rshift\n\nlocal abs = math.abs\nlocal char = string.char\n\nlocal ngx = ngx\nlocal ngx_re = ngx.re\nlocal re_match = ngx_re.match\nlocal re_gmatch = ngx_re.gmatch\nlocal re_gsub = ngx_re.gsub\n\nlocal NOT_MASK_FIRST = bnot(consts.MASK_FIRST)\nlocal NOT_MASK_LAST = bnot(consts.MASK_LAST)\n\nfunction _M.int_to_char_length(x)\n    return char(band(x, 0xff)) .. char(band(rshift(x, 8), 0xff)) ..\n            char(band(rshift(x, 16), 0xff)) .. char(band(rshift(x, 24), 0xff))\nend\n\nfunction _M.char_to_int_length(l)\n    return l:byte(1, 1) + lshift(l:byte(2, 2), 8) + lshift(l:byte(3, 3), 16) + lshift(l:byte(4, 4), 24)\nend\n\nfunction _M.is_mask_first(b)\n    return band(b, consts.MASK_FIRST) == consts.MASK_FIRST\nend\n\nfunction _M.is_mask_last(b)\n    return band(b, consts.MASK_LAST) == consts.MASK_LAST\nend\n\nlocal function tag_parser(tag)\n    return band(band(tag, NOT_MASK_FIRST), NOT_MASK_LAST)\nend\n\nfunction _M.packet_parser(header)\n    if #header ~= consts.T1K_HEADER_SIZE then\n        return true, nil, 0\n    end\n\n    local fb = header:byte(1, 1)\n    local finished = _M.is_mask_last(fb)\n    local tag = tag_parser(fb)\n    local length = _M.char_to_int_length(header:sub(2, 5))\n\n    return finished, tag, length\nend\n\nfunction _M.starts_with(str, start)\n    return str:sub(1, #start) == start\nend\n\nfunction _M.to_var_idx(o)\n    local var = o\n    local idx\n\n    local _, p = o:find(\":\")\n    if p then\n        var = o:sub(1, p - 1)\n        idx = tonumber(o:sub(p + 1))\n    end\n\n    var = re_gsub(var:lower(), \"-\", \"_\")\n    if not _M.starts_with(var, consts.NGX_HTTP_HEADER_PREFIX) then\n        var = consts.NGX_HTTP_HEADER_PREFIX .. var\n    end\n\n    return var, idx\nend\n\nfunction _M.get_indexed_element(str, idx)\n    if not str or not idx or idx == 0 then\n        return str\n    end\n\n    local it, err = re_gmatch(str, [[([^,\\s]+)]], \"jo\")\n    if err then\n        return nil\n    end\n\n    local t = {}\n    for m, e in it do\n        if e then\n            return nil\n        end\n        table.insert(t, m[1])\n    end\n\n    local len = #t\n    if len < abs(idx) then\n        return nil\n    end\n\n    return t[idx > 0 and idx or (len + idx + 1)]\nend\n\nfunction _M.get_event_id(str)\n    if not str then\n        return nil\n    end\n\n    local m, err = re_match(str, [[<!-- event_id: ([A-Za-z0-9]+)(?: TYPE: [a-zA-Z])? -->]], \"jo\")\n    if err then\n        return nil\n    end\n\n    if m then\n        return m[1]\n    end\n\n    return nil\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k/uuid.lua",
    "content": "local bit = require \"bit\"\nlocal ffi = require \"ffi\"\n\nlocal log = require \"resty.t1k.log\"\n\nlocal _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal C = ffi.C\nlocal N_BYTES = 32\nlocal random = math.random\n\nlocal nlog = ngx.log\n\nlocal warn_fmt = log.warn_fmt\n\nffi.cdef [[\n    int RAND_bytes(unsigned char *buf, int num);\n]]\n\nlocal function _rand_bytes(buf, len)\n    return C.RAND_bytes(buf, len)\nend\n\nlocal function rand_bytes(len)\n    local buf = ffi.new(\"char[?]\", len)\n    local ok, ret = pcall(_rand_bytes, buf, len)\n\n    if not ok or ret ~= 1 then\n        nlog(warn_fmt(\"call RAND_bytes failed: %s\", ret))\n        return nil\n    end\n\n    return ffi.string(buf, len)\nend\n\ndo\n    local band = bit.band\n    local bor = bit.bor\n    local tohex = bit.tohex\n\n    local fmt = string.format\n    local byte = string.byte\n\n    function _M.generate_v4()\n        local bytes = rand_bytes(N_BYTES)\n\n        -- fallback to math.random based method\n        if not bytes then\n            return (fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s',\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n\n                    tohex(bor(band(random(0, 255), 0x0F), 0x40), 2),\n                    tohex(random(0, 255), 2),\n\n                    tohex(bor(band(random(0, 255), 0x3F), 0x80), 2),\n                    tohex(random(0, 255), 2),\n\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2),\n                    tohex(random(0, 255), 2)))\n        end\n\n        return fmt('%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s',\n                tohex(byte(bytes, 1, 2), 2),\n                tohex(byte(bytes, 3, 4), 2),\n                tohex(byte(bytes, 5, 6), 2),\n                tohex(byte(bytes, 7, 8), 2),\n\n                tohex(byte(bytes, 9, 10), 2),\n                tohex(byte(bytes, 11, 12), 2),\n\n                tohex(bor(band(byte(bytes, 13, 14), 0x0F), 0x40), 2),\n                tohex(byte(bytes, 15, 16), 2),\n\n                tohex(bor(band(byte(bytes, 17, 18), 0x3F), 0x80), 2),\n                tohex(byte(bytes, 19, 20), 2),\n\n                tohex(byte(bytes, 21, 22), 2),\n                tohex(byte(bytes, 23, 24), 2),\n                tohex(byte(bytes, 25, 26), 2),\n                tohex(byte(bytes, 27, 28), 2),\n                tohex(byte(bytes, 29, 30), 2),\n                tohex(byte(bytes, 31, 32), 2))\n    end\nend\n\nreturn setmetatable(_M, {\n    __call = _M.generate_v4\n})\n"
  },
  {
    "path": "sdk/lua-resty-t1k/lib/resty/t1k.lua",
    "content": "local consts = require \"resty.t1k.constants\"\nlocal filter = require \"resty.t1k.filter\"\nlocal handler = require \"resty.t1k.handler\"\nlocal log = require \"resty.t1k.log\"\nlocal request = require \"resty.t1k.request\"\nlocal utils = require \"resty.t1k.utils\"\n\nlocal lower = string.lower\n\nlocal ngx = ngx\nlocal nlog = ngx.log\n\nlocal log_fmt = log.fmt\nlocal debug_fmt = log.debug_fmt\n\nlocal _M = {\n    _VERSION = '1.0.0'\n}\n\nlocal DEFAULT_T1K_CONNECT_TIMEOUT = 1000 -- 1s\nlocal DEFAULT_T1K_SEND_TIMEOUT = 1000 -- 1s\nlocal DEFAULT_T1K_READ_TIMEOUT = 1000 -- 1s\nlocal DEFAULT_T1K_REQ_BODY_SIZE = 1024 -- 1024 KB\nlocal DEFAULT_T1K_KEEPALIVE_SIZE = 256\nlocal DEFAULT_T1K_KEEPALIVE_TIMEOUT = 60 * 1000 -- 60s\n\nfunction _M.do_access(t, handle)\n    local ok, err, result\n    local opts = {}\n    t = t or {}\n\n    if not t.mode then\n        return true, nil, nil\n    end\n\n    opts.mode = lower(t.mode)\n    if opts.mode == consts.MODE_OFF then\n        nlog(debug_fmt(\"t1k is not enabled\"))\n        return true, nil, nil\n    end\n\n    if opts.mode ~= consts.MODE_OFF and opts.mode ~= consts.MODE_BLOCK and opts.mode ~= consts.MODE_MONITOR then\n        err = log_fmt(\"invalid t1k mode: %s\", t.mode)\n        return nil, err, nil\n    end\n\n    if not t.host then\n        err = log_fmt(\"invalid t1k host: %s\", t.host)\n        return nil, err, nil\n    end\n    opts.host = t.host\n\n    if utils.starts_with(opts.host, consts.UNIX_SOCK_PREFIX) then\n        opts.uds = true\n    else\n        if not tonumber(t.port) then\n            err = log_fmt(\"invalid t1k port: %s\", t.port)\n            return nil, err, nil\n        end\n        opts.port = tonumber(t.port)\n    end\n\n    opts.connect_timeout = t.connect_timeout or DEFAULT_T1K_CONNECT_TIMEOUT\n    opts.send_timeout = t.send_timeout or DEFAULT_T1K_SEND_TIMEOUT\n    opts.read_timeout = t.read_timeout or DEFAULT_T1K_READ_TIMEOUT\n    opts.req_body_size = t.req_body_size or DEFAULT_T1K_REQ_BODY_SIZE\n    opts.keepalive_size = t.keepalive_size or DEFAULT_T1K_KEEPALIVE_SIZE\n    opts.keepalive_timeout = t.keepalive_timeout or DEFAULT_T1K_KEEPALIVE_TIMEOUT\n\n    if t.remote_addr then\n        local var, idx = utils.to_var_idx(t.remote_addr)\n        opts.remote_addr_var = var\n        opts.remote_addr_idx = idx\n    end\n\n    ok, err, result = request.do_request(opts)\n    if not ok then\n        return ok, err, result\n    end\n\n    if handle and opts.mode == consts.MODE_BLOCK then\n        ok, err = _M.do_handle(result)\n    end\n\n    return ok, err, result\nend\n\nfunction _M.do_handle(t)\n    local ok, err = handler.handle(t)\n    return ok, err\nend\n\nfunction _M.do_header_filter()\n    filter.do_header_filter()\nend\n\nreturn _M\n"
  },
  {
    "path": "sdk/lua-resty-t1k/mainspec/lua-resty-t1k-main-0-0.rockspec",
    "content": "package = \"lua-resty-t1k-main\"\nversion = \"0-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    branch = \"main\",\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.0-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.0.0-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.0.0\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.1-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.0.1-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.0.1\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.2-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.0.2-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.0.2\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.0.3-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.0.3-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.0.3\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.0-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.0-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.0\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.1-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.1-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.1\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.2-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.2-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.2\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.3-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.3-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.3\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.4-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.4-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.4\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/rockspec/lua-resty-t1k-1.1.5-0.rockspec",
    "content": "package = \"lua-resty-t1k\"\nversion = \"1.1.5-0\"\nsource = {\n    url = \"git://github.com/chaitin/lua-resty-t1k\",\n    tag = \"v1.1.5\"\n}\n\ndescription = {\n    summary = \"Lua implementation of the T1K protocol for Chaitin SafeLine Web Application Firewall\",\n    detailed = [[\n      Check https://waf-ce.chaitin.cn/ for more information about Chaitin SafeLine Web Application Firewall.\n    ]],\n    homepage = \"https://github.com/chaitin/lua-resty-t1k\",\n    license = \"Apache License 2.0\",\n    maintainer = \"Xudong Wang <xudong.wang@chaitin.com>\"\n}\n\nbuild = {\n   type = \"builtin\",\n   modules = {\n    [\"resty.t1k\"] = \"lib/resty/t1k.lua\",\n    [\"resty.t1k.buffer\"] = \"lib/resty/t1k/buffer.lua\",\n    [\"resty.t1k.constants\"] = \"lib/resty/t1k/constants.lua\",\n    [\"resty.t1k.file\"] = \"lib/resty/t1k/file.lua\",\n    [\"resty.t1k.filter\"] = \"lib/resty/t1k/filter.lua\",\n    [\"resty.t1k.handler\"] = \"lib/resty/t1k/handler.lua\",\n    [\"resty.t1k.log\"] = \"lib/resty/t1k/log.lua\",\n    [\"resty.t1k.request\"] = \"lib/resty/t1k/request.lua\",\n    [\"resty.t1k.utils\"] = \"lib/resty/t1k/utils.lua\",\n    [\"resty.t1k.uuid\"] = \"lib/resty/t1k/uuid.lua\",\n   },\n}\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/buffer.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: buffer add\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local buffer = require \"resty.t1k.buffer\"\n            local b = buffer:new()\n            b:add(\"hello\")\n            b:add(\" \")\n            b:add(\"world\")\n            b:add(\"!\")\n            ngx.say(b[1], b[2], b[3], b[4])\n            ngx.say(b:len())\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world!\n12\n--- no_error_log\n[error]\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/file.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: read full file\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local file = require \"resty.t1k.file\"\n            local path = ngx.var.document_root .. \"/foo.bar\"\n            local ok, err, content = file.read(path, 2 ^ 15)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.print(table.concat(content))\n        }\n    }\n--- user_files eval\n[\n    [\"foo.bar\" => \"a\" x (2 ** 14) ],\n]\n--- request\nGET /t\n--- response_body eval\n\"a\" x (2 ** 14)\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: read partial file\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local file = require \"resty.t1k.file\"\n            local path = ngx.var.document_root .. \"/foo.bar\"\n            local ok, err, content = file.read(path, 2 ^ 13)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.print(table.concat(content))\n        }\n    }\n--- user_files eval\n[\n    [\"foo.bar\" => \"a\" x (2 ** 14) ],\n]\n--- request\nGET /t\n--- response_body eval\n\"a\" x (2 ** 13)\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: read negative bytes\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local file = require \"resty.t1k.file\"\n            local path = ngx.var.document_root .. \"/foo.bar\"\n            local ok, err, content = file.read(path, -1)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.print(table.concat(content))\n        }\n    }\n--- user_files\n>>> foo.bar\n--- request\nGET /t\n--- response_body eval\n\"\"\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: read empty file\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local file = require \"resty.t1k.file\"\n            local path = ngx.var.document_root .. \"/foo.bar\"\n            local ok, err, content = file.read(path, 1)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.print(table.concat(content))\n        }\n    }\n--- user_files\n>>> foo.bar\n--- request\nGET /t\n--- response_body eval\n\"\"\n--- no_error_log\n[error]\n\n\n\n=== TEST 5: read non-existent file\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local file = require \"resty.t1k.file\"\n            local ok, err, buffer = file.read(\"/opt/non_existent_file\", 0)\n            if not ok then\n                ngx.say(err)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\n/opt/non_existent_file: No such file or directory\n--- no_error_log\n[error]\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/filter.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 5);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: do_header_filter\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            ngx.ctx.t1k_extra_header = \"k1:v1\\nk2:v2\\nk3:v3\\n\"\n        }\n\n        header_filter_by_lua_block {\n            local filter = require \"resty.t1k.filter\"\n            filter.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"hi\")\n        }\n    }\n--- request\nGET /t\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- no_error_log\n[error]\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/handler.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: handle passed action\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local handler = require \"resty.t1k.handler\"\n\n            local t = {\n                action = \".\",\n            }\n\n            local ok, err = handler.handle(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: handle blocked action\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local handler = require \"resty.t1k.handler\"\n\n            local t = {\n                action = \"?\",\n                status = 405,\n                event_id = \"c0c039a7c348486eaffd9e2f9846b66b\",\n            }\n\n            local ok, err = handler.handle(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local filter = require \"resty.t1k.filter\"\n            filter.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"code\": 405, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c0c039a7c348486eaffd9e2f9846b66b\"}\n--- error_code eval\n\"405\"\n--- no_error_log\n[error]\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: handle unknown action\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local handler = require \"resty.t1k.handler\"\n\n            local t = {\n                action = \"~\"\n            }\n\n            local ok, err = handler.handle(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nlua-resty-t1k: unknown action from t1k server: ~\n\n\n\n=== TEST 4: handle nil result\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local handler = require \"resty.t1k.handler\"\n\n            local ok, err = handler.handle(nil)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nlua-resty-t1k: invalid result type: nil\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/integration.t",
    "content": "use Test::Nginx::Socket 'no_plan';\n\nour $MainConfig = <<'_EOC_';\nenv DETECTOR_IP;\n_EOC_\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n    lua_socket_log_errors off;\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: integration test blocked\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_headers\nContent-Type: application/json\n--- response_body_like eval\n'^{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \".*\"}$'\n--- error_code: 403\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 2: integration test blocked internal handle\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_headers\nContent-Type: application/json\n--- response_body_like eval\n'^{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \".*\"}$'\n--- error_code: 403\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 3: integration test blocked http2\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- http2\n--- request\nGET /t/shell.php\n--- response_headers\nContent-Type: application/json\n--- response_body_like eval\n'^{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \".*\"}$'\n--- error_code: 403\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 4: integration test blocked http2 internal handle\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- http2\n--- request\nGET /t/shell.php\n--- response_headers\nContent-Type: application/json\n--- response_body_like eval\n'^{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \".*\"}$'\n--- error_code: 403\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 5: integration test monitor\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 6: integration test monitor internal handle\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 7: integration test monitor http2\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- http2\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log eval\n[\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\", \"skip blocking\"]\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 8: integration test monitor http2 internal handle\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = os.getenv(\"DETECTOR_IP\"),\n                port = 8000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- http2\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log eval\n\"lua-resty-t1k: successfully connected to t1k server $ENV{DETECTOR_IP}:8000\"\n--- log_level: debug\n--- skip_eval\n4: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 9: integration test disabled\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"off\",\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: t1k is not enabled\nskip blocking\n--- log_level: debug\n\n\n\n=== TEST 10: integration test configuration priority\n--- main_config eval: $::MainConfig\n--- http_config eval: $::HttpConfig\n--- config\n    access_by_lua_block {\n        local t1k = require \"resty.t1k\"\n\n        local t = {\n            mode = \"block\",\n            host = os.getenv(\"DETECTOR_IP\"),\n            port = 8000,\n            connect_timeout = 1000,\n            send_timeout = 1000,\n            read_timeout = 1000,\n            req_body_size = 1024,\n            keepalive_size = 16,\n            keepalive_timeout = 10000,\n        }\n\n        local ok, err, result = t1k.do_access(t)\n        if not ok then\n            ngx.log(ngx.ERR, err)\n            return\n        end\n\n        if t.mode ~= \"block\" then\n            ngx.log(ngx.DEBUG, \"skip blocking\")\n            return\n        end\n\n        ok, err = t1k.do_handle(result)\n        if not ok then\n            ngx.log(ngx.ERR, err)\n            return\n        end\n    }\n\n    header_filter_by_lua_block {\n        local t1k = require \"resty.t1k\"\n        t1k.do_header_filter()\n    }\n\n    location /pass {\n        access_by_lua_block {\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n\n    location /block {\n        content_by_lua_block {\n            ngx.say(\"there must be a problem when you see this line\")\n        }\n    }\n--- request eval\n[\"GET /pass/shell.php\", \"GET /block/shell.php\"]\n--- response_body_like eval\n[\"passed\", '^{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \".*\"}$']\n--- error_code eval\n[200, 403]\n--- no_error_log\n[error]\n--- skip_eval\n6: not exists($ENV{DETECTOR_IP})\n\n\n\n=== TEST 11: integration test blocked extra headers\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_headers\nContent-Type: application/json\n--- response_body\n{\"code\": 405, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c0c039a7c348486eaffd9e2f9846b66b\"}\n--- error_code eval\n\"405\"\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 12: integration test blocked extra headers internal handle\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_headers\nContent-Type: application/json\n--- response_body\n{\"code\": 405, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c0c039a7c348486eaffd9e2f9846b66b\"}\n--- error_code eval\n\"405\"\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 13: integration test blocked extra headers with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_headers\nContent-Type: application/json\n--- response_body\n{\"code\": 405, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c0c039a7c348486eaffd9e2f9846b66b\"}\n--- error_code eval\n\"405\"\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 14: integration test passed extra headers\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00.\\xa3\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\"\n--- request\nGET /t\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 15: integration test passed extra headers internal handle\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00.\\xa3\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\"\n--- request\nGET /t\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 16: integration test passed extra headers with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00.\\xa3\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\"\n--- request\nGET /t\n--- response_headers\nk1: v1\nk2: v2\nk3: v3\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 17: integration test monitor extra headers\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- raw_response_headers_unlike eval\n'.*k1: v1\\r\\n.*'\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\nskip blocking\n--- log_level: debug\n\n\n\n=== TEST 18: integration test monitor extra headers internal handle\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t, true)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- raw_response_headers_unlike eval\n'.*k1: v1\\r\\n.*'\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 19: integration test monitor extra headers with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"monitor\",\n                host = \"unix:t1k.sock\",\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            if t.mode ~= \"block\" then\n                ngx.log(ngx.DEBUG, \"skip blocking\")\n                return\n            end\n\n            ok, err = t1k.do_handle(result)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        header_filter_by_lua_block {\n            local t1k = require \"resty.t1k\"\n            t1k.do_header_filter()\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\x23\\x12\\x00\\x00\\x00k1:v1\\x0ak2:v2\\x0ak3:v3\\x0a\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- raw_response_headers_unlike eval\n'.*k1: v1\\r\\n.*'\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\nskip blocking\n--- log_level: debug\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/log.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 2 + 2);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: err_fmt\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local log = require \"resty.t1k.log\"\n            ngx.log(log.err_fmt(\"%s - %04d - %.4f\", \"test\", 1, 1))\n        }\n    }\n--- request\nGET /t\n[error]\n--- error_log\nlua-resty-t1k: test - 0001 - 1.0000\n--- log_level: error\n\n\n\n=== TEST 2: warn_fmt\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local log = require \"resty.t1k.log\"\n            ngx.log(log.warn_fmt(\"%s - %04d - %.4f\", \"test\", 1, 1))\n        }\n    }\n--- request\nGET /t\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: test - 0001 - 1.0000\n--- log_level: warn\n\n\n\n=== TEST 3: debug_fmt\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local log = require \"resty.t1k.log\"\n            ngx.log(log.debug_fmt(\"%s - %04d - %.4f\", \"test\", 1, 1))\n        }\n    }\n--- request\nGET /t\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: test - 0001 - 1.0000\n--- log_level: debug\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/option.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3 + 1);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: do_access nil option\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local ok, err, _ = t1k.do_access(nil)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[debug]\n--- log_level: debug\n\n\n\n=== TEST 2: do_access disabled\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"off\",\n            }\n\n            local ok, err, _ = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: t1k is not enabled\n--- log_level: debug\n\n\n\n=== TEST 3: do_access invalid mode\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"invalid\",\n            }\n\n            local ok, err, _ = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t/shell.php\n--- response_body\npassed\n--- error_log\nlua-resty-t1k: invalid t1k mode: invalid\n\n\n\n=== TEST 4: do_access invalid host\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\"\n            }\n\n            local ok, err, _ = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nlua-resty-t1k: invalid t1k host: nil\n--- log_level: debug\n\n\n\n=== TEST 5: do_access invalid port\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local t1k = require \"resty.t1k\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\"\n            }\n\n            local ok, err, _ = t1k.do_access(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n        }\n\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nlua-resty-t1k: invalid t1k port: nil\n--- log_level: debug\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/request.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3 + 14);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: do_request blocked\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n            ngx.say(result[\"status\"])\n            ngx.say(result[\"event_id\"])\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_body\n?\n405\nc0c039a7c348486eaffd9e2f9846b66b\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 2: do_request blocked with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n            ngx.say(result[\"status\"])\n            ngx.say(result[\"event_id\"])\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_body\n?\n405\nc0c039a7c348486eaffd9e2f9846b66b\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 3: do_request passed\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00.\"\n--- request\nGET /t\n--- response_body\n.\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 4: do_request passed with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00.\"\n--- request\nGET /t\n--- response_body\n.\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 5: do_request trim request body\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00.\"\n--- request\nGET /t\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n--- response_body\n.\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: request body is too long: 123 bytes, cut to 64 bytes\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 6: do_request trim request body with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00.\"\n--- request\nGET /t\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n--- response_body\n.\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: request body is too long: 123 bytes, cut to 64 bytes\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 7: do_request refuse connection\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"result: \", result)\n        }\n    }\n--- request\nGET /t\n--- response_body\nresult: nil\n--- error_log\nfailed to connect to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 8: do_request refuse connection with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"result: \", result)\n        }\n    }\n--- request\nGET /t\n--- response_body\nresult: nil\n--- error_log\nfailed to connect to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 9: do_request timeout\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 100,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"result: \", result)\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply_delay: 200ms\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_body\nresult: nil\n--- error_log\nfailed to receive info packet from t1k server 127.0.0.1:18000: timeout\n--- log_level: debug\n\n\n\n=== TEST 10: do_request timeout with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 100,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"result: \", result)\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply_delay: 200ms\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- request\nGET /t/shell.php\n--- response_body\nresult: nil\n--- error_log\nfailed to receive info packet from t1k server unix:t1k.sock: timeout\n--- log_level: debug\n\n\n\n=== TEST 11: do_request invalid action\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"action: \", result[\"action\"])\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00~\"\n--- request\nGET /t\n--- response_body\naction: ~\n--- no_error_log\n[error]\n--- error_log\nsuccessfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 12: do_request invalid action with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 0.0625,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(\"action: \", result[\"action\"])\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\xc1\\x01\\x00\\x00\\x00~\"\n--- request\nGET /t\n--- response_body\naction: ~\n--- no_error_log\n[error]\n--- error_log\nsuccessfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n\n\n\n=== TEST 13: do_request remote address\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        access_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"ngx.var.http_x_real_ip or ngx.var.remote_addr is \", utils.get_indexed_element(ngx.var.http_x_real_ip) or ngx.var.remote_addr)\n            ngx.say(\"ngx.var.http_x_forwarded_for: 2 or ngx.var.remote_addr is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 2) or ngx.var.remote_addr)\n            ngx.say(\"ngx.var.http_x_forwarded_for: -2 or ngx.var.remote_addr is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -2) or ngx.var.remote_addr)\n            ngx.say(\"ngx.var.http_non_existent_header or ngx.var.remote_addr is \", utils.get_indexed_element(ngx.var.http_non_existent_header) or ngx.var.remote_addr)\n        }\n    }\n--- request\nGET /t\n--- more_headers\nX-Forwarded-For: 1.1.1.1, 2.2.2.2, 2001:db8:3333:4444:5555:6666:7777:8888, 3.3.3.3\nX-Real-IP: 100.100.100.100\n--- response_body\nngx.var.http_x_real_ip or ngx.var.remote_addr is 100.100.100.100\nngx.var.http_x_forwarded_for: 2 or ngx.var.remote_addr is 2.2.2.2\nngx.var.http_x_forwarded_for: -2 or ngx.var.remote_addr is 2001:db8:3333:4444:5555:6666:7777:8888\nngx.var.http_non_existent_header or ngx.var.remote_addr is 127.0.0.1\n--- no_error_log\n[error]\n\n\n\n=== TEST 14: do_request http2\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"127.0.0.1\",\n                port = 18000,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n            ngx.say(result[\"status\"])\n            ngx.say(result[\"event_id\"])\n        }\n    }\n--- tcp_listen: 18000\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- http2\n--- request\nGET /t/shell.php\n--- tcp_query eval\nqr/.*HTTP\\/2.0.*/\n--- response_body\n?\n405\nc0c039a7c348486eaffd9e2f9846b66b\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server 127.0.0.1:18000\n--- log_level: debug\n\n\n\n=== TEST 15: do_request http2 with unix domain socket\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local request = require \"resty.t1k.request\"\n\n            local t = {\n                mode = \"block\",\n                host = \"unix:t1k.sock\",\n                uds = true,\n                connect_timeout = 1000,\n                send_timeout = 1000,\n                read_timeout = 1000,\n                req_body_size = 1024,\n                keepalive_size = 16,\n                keepalive_timeout = 10000,\n            }\n\n            local ok, err, result = request.do_request(t)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n            end\n\n            ngx.say(result[\"action\"])\n            ngx.say(result[\"status\"])\n            ngx.say(result[\"event_id\"])\n        }\n    }\n--- tcp_listen: t1k.sock\n--- tcp_reply eval\n\"\\x41\\x01\\x00\\x00\\x00?\\x02\\x03\\x00\\x00\\x00405\\xa4\\x33\\x00\\x00\\x00<!-- event_id: c0c039a7c348486eaffd9e2f9846b66b -->\"\n--- http2\n--- request\nGET /t/shell.php\n--- tcp_query eval\nqr/.*HTTP\\/2.0.*/\n--- response_body\n?\n405\nc0c039a7c348486eaffd9e2f9846b66b\n--- no_error_log\n[error]\n--- error_log\nlua-resty-t1k: successfully connected to t1k server unix:t1k.sock\n--- log_level: debug\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/utils.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: int_to_char_length\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"255 to char length: \", utils.int_to_char_length(255))\n            ngx.print(\"16777216 to char length: \", utils.int_to_char_length(16777216))\n        }\n    }\n--- request\nGET /t\n--- response_body eval\n\"255 to char length: \\x{ff}\\x{00}\\x{00}\\x{00}\n16777216 to char length: \\x{00}\\x{00}\\x{00}\\x{01}\"\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: int_to_char_length\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"0xff 0x00 0x00 0x00 to int length: \", utils.char_to_int_length(\"\\xff\\x00\\x00\\x00\"))\n            ngx.say(\"0x00 0x00 0x00 0x01 to int length: \", utils.char_to_int_length(\"\\x00\\x00\\x00\\x01\"))\n        }\n    }\n--- request\nGET /t\n--- response_body\n0xff 0x00 0x00 0x00 to int length: 255\n0x00 0x00 0x00 0x01 to int length: 16777216\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: is_mask_first\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"0x30 is MASK FIRST: \", utils.is_mask_first(0x30))\n            ngx.say(\"0x41 is MASK FIRST: \", utils.is_mask_first(0x41))\n            ngx.say(\"0xc0 is MASK FIRST: \", utils.is_mask_first(0xc0))\n            ngx.say(\"0xc1 is MASK FIRST: \", utils.is_mask_first(0xc1))\n        }\n    }\n--- request\nGET /t\n--- response_body\n0x30 is MASK FIRST: false\n0x41 is MASK FIRST: true\n0xc0 is MASK FIRST: true\n0xc1 is MASK FIRST: true\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: is_mask_last\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"0x41 is MASK LAST: \", utils.is_mask_last(65))\n            ngx.say(\"0x80 is MASK LAST: \", utils.is_mask_last(128))\n        }\n    }\n--- request\nGET /t\n--- response_body\n0x41 is MASK LAST: false\n0x80 is MASK LAST: true\n--- no_error_log\n[error]\n\n\n\n=== TEST 5: packet_parser unfinished\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            local finished, tag, length = utils.packet_parser(\"\\x41\\x59\\x00\\x00\\x00\")\n            ngx.say(\"finished: \", finished)\n            ngx.say(\"tag == TAG_HEAD: \", tag == 1)\n            ngx.say(\"length: \", 89)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfinished: false\ntag == TAG_HEAD: true\nlength: 89\n--- no_error_log\n[error]\n\n\n\n=== TEST 6: packet_parser finished\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            local finished, tag, length = utils.packet_parser(\"\\xa0\\x08\\x00\\x00\\x00\")\n            ngx.say(\"finished: \", finished)\n            ngx.say(\"tag == TAG_VERSION: \", tag == 32)\n            ngx.say(\"length: \", 8)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfinished: true\ntag == TAG_VERSION: true\nlength: 8\n--- no_error_log\n[error]\n\n\n\n=== TEST 7: starts_with\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"http://www.baidu.com starts with \\\"http\\\": \", utils.starts_with(\"http://www.baidu.com\", \"http\"))\n            ngx.say(\"http://www.baidu.com starts with \\\"https\\\": \", utils.starts_with(\"http://www.baidu.com\", \"https\"))\n        }\n    }\n--- request\nGET /t\n--- response_body\nhttp://www.baidu.com starts with \"http\": true\nhttp://www.baidu.com starts with \"https\": false\n--- no_error_log\n[error]\n\n\n\n=== TEST 8: to_var_idx\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            local fmt = string.format\n            ngx.say(fmt(\"http_x_real_ip is %s and %s\", utils.to_var_idx(\"http_x_real_ip\")))\n            ngx.say(fmt(\"X_REAL_IP is %s and %s\", utils.to_var_idx(\"X_REAL_IP\")))\n            ngx.say(fmt(\"X-Forwarded-For: 1 is %s and %d\", utils.to_var_idx(\"X-Forwarded-For: 1\")))\n            ngx.say(fmt(\"X-Forwarded-For: -1 is %s and %d\", utils.to_var_idx(\"X-Forwarded-For: -1\")))\n            ngx.say(fmt(\"X-FORWARDED-FOR:100 is %s and %d\", utils.to_var_idx(\"X-FORWARDED-FOR:-100\")))\n        }\n    }\n--- request\nGET /t\n--- response_body\nhttp_x_real_ip is http_x_real_ip and nil\nX_REAL_IP is http_x_real_ip and nil\nX-Forwarded-For: 1 is http_x_forwarded_for and 1\nX-Forwarded-For: -1 is http_x_forwarded_for and -1\nX-FORWARDED-FOR:100 is http_x_forwarded_for and -100\n--- no_error_log\n[error]\n\n\n\n=== TEST 9: get_indexed_element\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(\"X-Forwarded-For: 1 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 1))\n            ngx.say(\"X-Forwarded-For: 2 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 2))\n            ngx.say(\"X-Forwarded-For: 3 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 3))\n            ngx.say(\"X-Forwarded-For: 4 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 4))\n            ngx.say(\"X-Forwarded-For: 5 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 5))\n            ngx.say(\"X-Forwarded-For: -1 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -1))\n            ngx.say(\"X-Forwarded-For: -2 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -2))\n            ngx.say(\"X-Forwarded-For: -3 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -3))\n            ngx.say(\"X-Forwarded-For: -4 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -4))\n            ngx.say(\"X-Forwarded-For: -5 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, -5))\n            ngx.say(\"X-Forwarded-For: 0 is \", utils.get_indexed_element(ngx.var.http_x_forwarded_for, 0))\n            ngx.say(\"X-Non-Existent-Header is \", utils.get_indexed_element(ngx.var.http_non_existent_header))\n            ngx.say(\"X-Real-IP is \", utils.get_indexed_element(ngx.var.http_x_real_ip))\n            ngx.say(\"X-Real-IP: 1 is \", utils.get_indexed_element(ngx.var.http_x_real_ip, 1))\n            ngx.say(\"X-Real-IP: 2 is \", utils.get_indexed_element(ngx.var.http_x_real_ip, 2))\n            ngx.say(\"X-Real-IP: -1 is \", utils.get_indexed_element(ngx.var.http_x_real_ip, -1))\n            ngx.say(\"X-Real-IP: -2 is \", utils.get_indexed_element(ngx.var.http_x_real_ip, -2))\n        }\n    }\n--- request\nGET /t\n--- more_headers\nX-Forwarded-For: 1.1.1.1, 2.2.2.2\nX-Forwarded-For: 3.3.3.3, 4.4.4.4\nX-Real-IP: 10.10.10.10\n--- response_body\nX-Forwarded-For: 1 is 1.1.1.1\nX-Forwarded-For: 2 is 2.2.2.2\nX-Forwarded-For: 3 is 3.3.3.3\nX-Forwarded-For: 4 is 4.4.4.4\nX-Forwarded-For: 5 is nil\nX-Forwarded-For: -1 is 4.4.4.4\nX-Forwarded-For: -2 is 3.3.3.3\nX-Forwarded-For: -3 is 2.2.2.2\nX-Forwarded-For: -4 is 1.1.1.1\nX-Forwarded-For: -5 is nil\nX-Forwarded-For: 0 is 1.1.1.1, 2.2.2.2, 3.3.3.3, 4.4.4.4\nX-Non-Existent-Header is nil\nX-Real-IP is 10.10.10.10\nX-Real-IP: 1 is 10.10.10.10\nX-Real-IP: 2 is nil\nX-Real-IP: -1 is 10.10.10.10\nX-Real-IP: -2 is nil\n--- no_error_log\n[error]\n\n\n\n=== TEST 10: get_event_id\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local utils = require \"resty.t1k.utils\"\n            ngx.say(utils.get_event_id(\"<!-- event_id: 0988987de04844c7a3ce6d27865c9513 -->\"))\n            ngx.say(utils.get_event_id(\"<!-- event_id: 8bae6adf33864c7f8bf715a9b7a65b2c TYPE: A -->\"))\n            ngx.say(utils.get_event_id(\"<!-- TYPE B -->\"))\n        }\n    }\n--- request\nGET /t\n--- response_body\n0988987de04844c7a3ce6d27865c9513\n8bae6adf33864c7f8bf715a9b7a65b2c\nnil\n--- no_error_log\n[error]\n"
  },
  {
    "path": "sdk/lua-resty-t1k/t/uuid.t",
    "content": "use Test::Nginx::Socket;\n\nour $HttpConfig = <<'_EOC_';\n    lua_package_path \"lib/?.lua;/usr/local/share/lua/5.1/?.lua;;\";\n_EOC_\n\nrepeat_each(3);\n\nplan tests => repeat_each() * (blocks() * 3);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: generate_v4\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local uuid = require \"resty.t1k.uuid\"\n            ngx.say(uuid.generate_v4())\n        }\n    }\n--- request\nGET /t\n--- response_body_like\n^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$\n--- no_error_log\n[error]\n"
  },
  {
    "path": "version.json",
    "content": "{\n  \"latest_version\": \"v9.2.7\",\n  \"rec_version\": \"v9.2.7\",\n  \"lts_version\": \"v9.1.0-lts\"\n}\n"
  },
  {
    "path": "yanshi/.gitignore",
    "content": "*.o\n*.dot\n*.output\n/build\n/src/parser.cc\n/src/parser.hh\n/src/lexer.cc\n/src/lexer.hh\n"
  },
  {
    "path": "yanshi/Makefile",
    "content": "CPPFLAGS := -g3 -std=c++1y -Isrc -I. -DHAVE_READLINE\n\nifeq ($(build),release)\n  BUILD := release\n  CPPFLAGS += -Os\nelse\n  BUILD := build\n  CPPFLAGS += -fsanitize=undefined,address -DDEBUG\n  LDLIBS := -lasan -lubsan\nendif\n\nLDLIBS += -licuuc -lreadline\nSRC := $(filter-out src/lexer.cc src/parser.cc, $(wildcard src/*.cc)) src/lexer.cc src/parser.cc\nOBJ := $(addprefix $(BUILD)/,$(subst src/,,$(SRC:.cc=.o)))\nUNITTEST_SRC := $(wildcard unittest/*.cc)\nUNITTEST_EXE := $(subst unittest/,,$(UNITTEST_SRC:.cc=))\n\nall: $(BUILD)/yanshi # unittest\n\nunittest: $(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE))\n\t$(foreach x,$(addprefix $(BUILD)/unittest/,$(UNITTEST_EXE)),$x && ) :\n\nsinclude $(OBJ:.o=.d)\n\n# FIXME\n$(BUILD)/repl.o: src/lexer.hh\n\n$(BUILD) $(BUILD)/unittest:\n\tmkdir -p $@\n\n$(BUILD)/yanshi: $(OBJ)\n\t$(LINK.cc) $^ $(LDLIBS) -o $@\n\n$(BUILD)/%.o: src/%.cc | $(BUILD)\n\t$(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<\n\t$(COMPILE.cc) $< -o $@\n\n$(BUILD)/unittest/%: unittest/%.cc $(wildcard unittest/*.hh) $(filter-out $(BUILD)/main.o,$(OBJ)) | $(BUILD)/unittest\n\t$(CXX) $(CPPFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<\n\t$(LINK.cc) $(filter-out %.hh,$^) $(LDLIBS) -o $@\n\nsrc/lexer.cc src/lexer.hh: src/lexer.l\n\tflex --header-file=src/lexer.hh -o src/lexer.cc $<\n\nsrc/parser.cc src/parser.hh: src/parser.y src/common.hh src/location.hh src/option.hh src/syntax.hh\n\tbison --defines=src/parser.hh -o src/parser.cc $<\n\n$(BUILD)/loader.o: src/parser.hh\n$(BUILD)/parser.o: src/lexer.hh\n$(BUILD)/lexer.o: src/parser.hh\n\nclean:\n\t$(RM) -r build release\n\ndistclean: clean\n\t$(RM) src/{lexer,parser}.{cc,hh}\n\n.PHONY: all clean distclean\n"
  },
  {
    "path": "yanshi/README.md",
    "content": "# 偃师 (yanshi)\n\nyanshi is a finite state automaton generator like Ragel. Use inline operators to embed C++ code in the recognition of a language. yanshi is enhanced with features to approximate context-free grammar:\n\n- Approximation of substring grammar\n- Approximation of recursive automaton to match expressions with recursion.\n\nThe motivation to create yanshi is that Ragel does not provide a mechanism to serialize its representation of finite state automata, making it difficult to post-process generated automata and obtain the substring grammar recognizer.\n\nLater on, I found a simplified SQL grammar might contain more than 10000 states. It was not only slow to generate the automaton, making it hard to do trial-and-error experiments, but wasted memory to store the automaton. I introduced `CollapseExpr` to allow circular references.\n\n`CallExpr` takes one step further, maintains a return address stack to imitate function calls. It can be seen as an augmented `CollapseExpr`, removing a lot of false positive cases.\n\n## Name\n\nFrom <https://en.wikipedia.org/wiki/Automaton>:\n\n<blockquote>\nIn ancient China, a curious account of automata is found in the Lie Zi text (列子), written in the 3rd century BC. Within it there is a description of a much earlier encounter between King Mu of Zhou (周穆王, 1023-957 BC) and a mechanical engineer known as Yan Shi (偃师), an 'artificer'.\n</blockquote>\n\n## Build\n\n* Debug: `make`\n* Release: `make build=release`\n\n## Getting Started\n\n* Create file `a.ys`:\n  ```\n  export foo = 'hello'\n  ```\n\n  Run `yanshi -S a.ys -o /tmp/a.cc` to generate a C++ file from the yanshi source file `a.ys`.\n\n  + `yanshi_foo_start`: the start state is 0. States are represented by natural numbers.\n  + `yanshi_foo_is_final`: leave aside `ret_stack` and look at the last line, it checks whether `u` is one of the final states.\n  + `yanshi_foo_transit`: leave aside `ret_stack`, `u` is the current state and `c` is the next input codepoint or label.\n\n  With the `-S` option, yanshi will generate a standalone C++ file.\n  ```\n  % make -C /tmp a\n  make: Entering directory '/tmp'\n  g++     a.cc   -o a\n  make: Leaving directory '/tmp'\n  % /tmp/a hello\n  0 h 1 e 2 l 3 l 4 o 5\n  len: 5\n  pref: 5\n  state: 5\n  final: true\n  % /tmp/a\n  hello<press C-d>0 h 1 e 2 l 3 l 4 o 5\n  len: 5\n  pref: 5\n  state: 5\n  final: true\n  ```\n\n  States are yellow and interleaved with transition labels. Final states are bold yellow.\n  + `len`: the length of input codepoints or labels\n  + `pref`: the length of the longest prefix that does not enter the dead state\n  + `state`: the state entered after consuming the input\n  + `final`: whether the state is one of final states\n\n* Interactive mode\n\n  The `-i` option enables interactive mode.\n  ```\n  % yanshi -i a.ys\n  Testing foo\n  foo :: DefineStmt\n  .integer mode\n  Commands available from the prompt:\n    .automaton    dump automaton\n    .assoc        dump associated AST Expr for each state\n    .help         display this help\n    .integer      input is a list of non-negative integers, macros(#define) or ''  quoted strings\n    .macro        display defined macros\n    .string       input is a string\n    .stmt <ident> change target DefineStmt to <ident>\n    .quit         exit interactive mode\n  λ 104 101 108 108 111\n  0 104 1 101 2 108 3 108 4 111 5\n  export foo = 'hello':\n  λ .string\n  .string mode\n  λ hello\n  0 h 1 e 2 l 3 l 4 o 5\n  export foo = 'hello':\n  λ\n  ```\n\n* Regex-like syntax\n  ```\n  export hello = [gh] 'e' l{2} 'o'\n  l = 'l'\n  ```\n\n  `[gh]` is a bracket expression and `l{2}` denotes to matches `l` at least twice. This grammar matches `hello`, `gello`, `helllo`, ...\n\n  + Union: `c = a | b`\n  + Intersection: `c = a && b`\n  + Difference: `c = a - b`\n  + Concatenation: `c = a b`\n  + Complement: `c = ~ a`\n\n* Actions (embedded C++ code)\n  ```\n  c++ {\n  #include <stdio.h>\n  }\n  export hello = '喵' @ { puts(\"meow\"); } {2}\n  ```\n  I have not thought clearly on the implementation. The executing point may be counter-intuitive.\n\n* Modules\n  ```\n  # a.ys\n  import 'b.ys' as B # B::bar\n  import 'b.ys' # qux\n\n  export foo = B::bar | qux\n  bar = '4'\n\n  # b.ys\n  bar = '3'\n  qux = '5'\n  ```\n\n* Substring grammar\n  Specify the `--substring-grammar` option to generate code for substring grammar. That is, the generated code matches every substring of the grammar. The implementation creates a new start state and a new final state, connects the start state to the old start state, and old final states to the new final state.\n\n* `EmbedExpr`, reference a nonterminal without modifiers\n   ```\n   foo = bar\n   bar = [0-9]\n   ```\n   The complete automaton of bar will be duplicated in each reference site. If the referenced automaton is large, `EmbedExpr` will significantly increase the number of states. `EmbedExpr` defines dependencies among states and no cyclic dependency is allowed.\n\n* `CollapseExpr`, reference a nonterminal with the `!` modifier\n  ```\n  export foo = 'pre' !bar 'post'\n  bar = [\\u0300-\\u034E]\n  quz = 'meow' !bar 'meow'\n  ```\n  The final state of `'pre'` and the start state of `'post'` will be connected by a special directed arc. When exporting, an epsilon transition will be added from the tail of the arc to the start state of `bar`, others will be added from the final states of `bar` to the head of the arc. `CollapseExpr` behaves like function calls, however, the return address is not preserved (hence the name `CollapseExpr`) and the state may go to other call sites. In this example, the state may go to either `foo` or `quz` after traveling through `bar`, causing false positives.\n\n* `CallExpr`, reference a nonterminal with the `&` modifier\n  ```\n  export foo = 'pre' &bar 'post'\n  bar = '4'\n  ```\n  This is a refinement of `CollapseExpr`. Suppose state `&B` is contained in `A`'s definition (`A` calls `B`). `&B` will be represented as an pseudo arc (`u -> v`), where `u` is the state before `&B` and `v` is the state after `&B`. If arcs of `u` do not collide with arcs of `B`, the transition function will push `v` to the return stack if current state set contains `u` and there is no other transition. Note `B` is disconnected from `A`, which is different from the `CollapseExpr` case. The machine will transit on automaton `B` greedily. If there is no transition, it will pop a return address(`v` in this case) and jumps to it.\n\n## Contrib\n\n### Vim\n\nSyntax highlighting, and a syntax checking plugin for Synaptics\n\n```zsh\nln -sr contrib/vim/compiler/yanshi.vim ~/.vim/compiler/\nln -sr contrib/vim/ftdetect/yanshi.vim ~/.vim/ftdetect/\nln -sr contrib/vim/ftplugin/yanshi.vim ~/.vim/ftplugin/\nln -sr contrib/vim/syntax/yanshi.vim ~/.vim/syntax/\nln -sr contrib/vim/syntax_checker/yanshi ~/.vim/syntax_checker/\n```\n\n### Zsh\n\nCommand line completion\n\n```\n# ~/.zshrc\nfpath=(~/.zsh $fpath)\n\n# ln -sr contrib/zsh/_yanshi ~/.zsh/\n```\n\n## Internals\n\n```\nsrc\n  common.{cc,hh}\n  main.{cc,hh}\n  syntax.{cc,hh}\n  loader.{cc,hh}\n  fsa.{cc,hh}\n  fsa_anno.{cc,hh}\n  compiler.{cc,hh}\n  parser.y\n  lexer.l\n  location.cc\n```\n\n* Lex `lexer.l`\n* Parse and generate a syntax tree `parser.y`\n* `loader.cc`\n  + Get a list of definitions\n  + Recursively load for each `import`\n  + Resolve references and associate uses to definitions\n  + Build a dependency graph from `EmbedExpr`\n  + Compile automaton for each nonterminal in topological order. `CollapseExpr` and `CallExpr` are represented by special directed arcs.\n  + Generate code for `export` nonterminals, resolving `CollapseExpr` and `CallExpr`\n\n### Finite state automaton\n\nEach node of the syntax tree corresponds to an automaton. The parent builds an automaton from its children according to the semantics. The automaton of the parent may contain states from the automaton of one of the children, or it is a state introduced by the parent.\n\n`assoc[i]` records the associative nodes in the automaton tree (which part of the syntax tree has associations with this state) and positions (start state, final state or inner state) for state `i`. It serves three purposes:\n\n* Check which action should be triggered\n* Look for inner states (neither start nor final) in the implementation of substring grammar\n* Check whether it is associated to a `CallExpr` or `CollapseExpr`\n\n### `CollapseExpr`\n"
  },
  {
    "path": "yanshi/contrib/vim/compiler/yanshi.vim",
    "content": "\"if exists('current_compiler')\n\"  finish\n\"endif\nlet current_compiler = 'yanshi'\n\nif exists(':CompilerSet') != 2\n  command -nargs=* CompilerSet setlocal <args>\nendif\nCompilerSet errorformat=\n      \\%E%f\\ %l:%c-%*\\\\d\\ error\\ %m,\n      \\%E%f\\ %l-%*\\\\d:%c-%*\\\\d\\ error\\ %m,\n      \\%W%f\\ %l:%c-%*\\\\d\\ warning\\ %m,\n      \\%W%f\\ %l-%*\\\\d:%c-%*\\\\d\\ warning\\ %m,\n      \\%C%.%#\nCompilerSet makeprg=yanshi\\ -d0\\ -c\\ $*\\ %\n"
  },
  {
    "path": "yanshi/contrib/vim/ftdetect/yanshi.vim",
    "content": "au BufRead,BufNewFile *.yanshi setf yanshi\nau BufRead,BufNewFile *.ys setf yanshi\n"
  },
  {
    "path": "yanshi/contrib/vim/ftplugin/yanshi.vim",
    "content": "if exists('b:did_ftplugin')\n  finish\nendif\nlet b:did_ftplugin = 1\n\ncompiler yanshi\n"
  },
  {
    "path": "yanshi/contrib/vim/syntax/yanshi.vim",
    "content": "if exists('b:current_syntax')\n  finish\nendif\n\nsyn cluster yanshiCommentGroup contains=yanshiTodo\nsyn include @yanshiCcode syntax/cpp.vim\nsyn keyword yanshiAction action\nsyn keyword yanshiMacro semicolon nosemicolon\nsyn keyword yanshiStorageClass export intact\nsyn keyword yanshiTodo contained TODO FIXME XXX\nsyn match yanshiCpp 'c++'\nsyn match yanshiActionOperator '[>$@%]'\nsyn match yanshiCall '\\^\\w\\+\\(::\\w\\+\\)\\?'\nsyn match yanshiCollapse '!\\w\\+\\(::\\w\\+\\)\\?'\nsyn match yanshiHighOp '[+\\*?]'\nsyn match yanshiIdent '\\w\\+\\(::\\w\\+\\)\\?'\nsyn match yanshiCpp display \"^c++\\s*\" skipwhite nextgroup=yanshiBrace\nsyn match yanshiImport display \"^import\\s*\" contains=yanshiImported\nsyn match yanshiLowOp '[-&|]'\nsyn match yanshiSpecial display contained \"\\\\\\(x\\x\\x\\|.\\|$\\)\"\nsyn region yanshiBrace matchgroup=Delimiter start='{' end='}' fold contains=@yanshiCcode\nsyn region yanshiBracket start='\\[' skip=+\\\\\\\\\\|\\\\]+ end=']'\nsyn region yanshiComment start='/\\*' end='\\*/' keepend contains=@yanshiCommentGroup,@Spell\nsyn region yanshiImported display contained start=\"+\" skip=+\\\\\\\\\\|\\\\\"+ end=+\"+\nsyn region yanshiLineComment start='#\\|//' skip='\\\\$' end='$' keepend contains=@yanshiCommentGroup,@Spell\nsyn region yanshiPreprocess start=\"#define\" skip=\"\\\\$\" end=\"$\" keepend\nsyn region yanshiQQString start=+\"+ skip=+\\\\.+ end=+\"+ contains=yanshiSpecial\nsyn region yanshiQString start=+'+ skip=+\\\\.+ end=+'+\n\nsyn region yanshiDefineStmt start='^\\w\\+\\s*[=:]' end='$' skipnl contains=@yanshiExpr,yanshiComment,yanshiLineComment,yanshiParen0\n\nsyn cluster yanshiExpr contains=yanshiActionOperator,yanshiBrace,yanshiBracket,yanshiCall,yanshiCollapse,yanshiIdent,yanshiHighOp,yanshiLowOp,yanshiQString,yanshiQQString,\nsy region yanshiParen0 matchgroup=yanshiParen0 start='(' end=')' contains=@yanshiExpr,yanshiParen1\nsy region yanshiParen1 matchgroup=yanshiParen1 start='(' end=')' contains=@yanshiExpr,yanshiParen2 contained\nsy region yanshiParen2 matchgroup=yanshiParen2 start='(' end=')' contains=@yanshiExpr,yanshiParen3 contained\nsy region yanshiParen3 matchgroup=yanshiParen3 start='(' end=')' contains=@yanshiExpr,yanshiParen4 contained\nsy region yanshiParen4 matchgroup=yanshiParen4 start='(' end=')' contains=@yanshiExpr,yanshiParen5 contained\nsy region yanshiParen5 matchgroup=yanshiParen5 start='(' end=')' contains=@yanshiExpr,yanshiParen0 contained\nhi yanshiParen0 ctermfg=brown guifg=#3bb9ff\nhi yanshiParen1 ctermfg=DarkBlue guifg=#f88017\nhi yanshiParen2 ctermfg=darkgray guifg=#5efb6e\nhi yanshiParen3 ctermfg=darkgreen guifg=#f62817\nhi yanshiParen4 ctermfg=darkcyan guifg=#fdd017\nhi yanshiParen5 ctermfg=darkmagenta guifg=#faafba\n\nhi link yanshiIdent          Identifier\n\"TODO color mismatch of {}\n\"hi link yanshiBrace          Statement\n\"hi link yanshiDefineStmt     Statement\nhi def link yanshiCall           Constant\nhi def link yanshiCollapse       Constant\nhi def link yanshiAction         Structure\nhi def link yanshiActionOperator Type\nhi def link yanshiBracket        Function\nhi def link yanshiCpp            Structure\nhi def link yanshiComment        Comment\nhi def link yanshiHighOp         Operator\nhi def link yanshiImport         Include\nhi def link yanshiImported       String\nhi def link yanshiLineComment    Comment\nhi def link yanshiLowOp          Conditional\nhi def link yanshiMacro          Macro\nhi def link yanshiPreprocess     Macro\nhi def link yanshiQQString       String\nhi def link yanshiQString        String\nhi def link yanshiSpecial        SpecialChar\nhi def link yanshiStorageClass   StorageClass\nhi def link yanshiTodo           Todo\n\nlet b:current_syntax = 'yanshi'\n"
  },
  {
    "path": "yanshi/contrib/vim/syntax_checkers/yanshi/yanshi.vim",
    "content": "if exists('g:loaded_syntastic_yanshi_yanshi_checker')\n  finish\nendif\nlet g:loaded_syntastic_yanshi_yanshi_checker = 1\n\nlet s:save_cpo = &cpo\nset cpo&vim\n\nfu! SyntaxCheckers_yanshi_yanshi_GetLocList() dict\n  let makeprg = self.makeprgBuild({ 'args': '-d0 -c' })\n\n  let errorformat =\n        \\ '%C  %.%#,'.\n        \\ '%E%f %l:%c-%*\\d error %m,'.\n        \\ '%E%f %l-%*\\d:%c-%*\\d error %m,'.\n        \\ '%W%f %l:%c-%*\\d warning %m,'.\n        \\ '%W%f %l-%*\\d:%c-%*\\d warning %m'\n\n  return SyntasticMake({\n        \\ 'makeprg': makeprg,\n        \\ 'errorformat': errorformat })\nendf\n\ncall g:SyntasticRegistry.CreateAndRegisterChecker({\n      \\ 'filetype': 'yanshi',\n      \\ 'name': 'yanshi'})\n\nlet &cpo = s:save_cpo\nunlet s:save_cpo\n"
  },
  {
    "path": "yanshi/contrib/zsh/_yanshi",
    "content": "#compdef yanshi\n\n_arguments \\\n  '(-b --bytes)'{-b,--bytes}'[make labels range over \\[0,256), Unicode literals will be treated as UTF-8 bytes]' \\\n  '(-c --check)'{-c,--check}'[check syntax & use/def]' \\\n  '-C[generate C source code (default: C++)]' \\\n  '(-d --debug)'{-d,--debug}'+[debug level]:level:(0 1 2 3 4 5)' \\\n  '--dump-action[dump associated actions for each edge]' \\\n  '--dump-assoc[dump associated AST Expr for each state]' \\\n  '--dump-automaton[dump automata]' \\\n  '--dump-embed[dump statistics of EmbedExpr]' \\\n  '--dump-module[dump module use/def/...]' \\\n  '--dump-tree[dump AST]' \\\n  '(-G --graph)'{-G,--graph}'[output a Graphviz dot file]' \\\n  '(-I --import)'{-I,--import}'=[add <dir> to search path for \"import\"]' \\\n  '(-i --interactive)'{-i,--interactive}'[interactive mode]' \\\n  '(-k --keep-inaccessible)'{-k,--keep-inaccessible}'[do not perform accessible/co-accessible]' \\\n  '(-l --debug-output)'{-l,--debug-output}'=[filename for debug output]:file:_files' \\\n  '--max-return-stack=[max length of return stack in C generator]:len:' \\\n  '(-o --output)'{-o,--output}'=[.cc output filename]:file:_files' \\\n  '(-O --output-header)'{-O,--output-header}'=[.hh output filename]:file:_files' \\\n  '(-s --substring-grammar)'{-s,--substring-grammar}'[construct regular approximation of the substring grammar. Inner states of nonterminals labeled 'intact' are not connected to start/final]' \\\n  '(-h --help)'{-h,--help}'[display this help]' \\\n  '1:file:_files -g \"*.{ys,yanshi}\"'\\\n"
  },
  {
    "path": "yanshi/src/common.cc",
    "content": "#include \"common.hh\"\n#include \"option.hh\"\n\n#include <errno.h>\n#include <execinfo.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/time.h>\n#include <sysexits.h>\n#include <time.h>\n#include <unistd.h>\n\n///// Error\n\nstatic const char *ENAME[] = {\n    /*   0 */ \"\",\n    /*   1 */ \"EPERM\", \"ENOENT\", \"ESRCH\", \"EINTR\", \"EIO\", \"ENXIO\",\n    /*   7 */ \"E2BIG\", \"ENOEXEC\", \"EBADF\", \"ECHILD\",\n    /*  11 */ \"EAGAIN/EWOULDBLOCK\", \"ENOMEM\", \"EACCES\", \"EFAULT\",\n    /*  15 */ \"ENOTBLK\", \"EBUSY\", \"EEXIST\", \"EXDEV\", \"ENODEV\",\n    /*  20 */ \"ENOTDIR\", \"EISDIR\", \"EINVAL\", \"ENFILE\", \"EMFILE\",\n    /*  25 */ \"ENOTTY\", \"ETXTBSY\", \"EFBIG\", \"ENOSPC\", \"ESPIPE\",\n    /*  30 */ \"EROFS\", \"EMLINK\", \"EPIPE\", \"EDOM\", \"ERANGE\",\n    /*  35 */ \"EDEADLK/EDEADLOCK\", \"ENAMETOOLONG\", \"ENOLCK\", \"ENOSYS\",\n    /*  39 */ \"ENOTEMPTY\", \"ELOOP\", \"\", \"ENOMSG\", \"EIDRM\", \"ECHRNG\",\n    /*  45 */ \"EL2NSYNC\", \"EL3HLT\", \"EL3RST\", \"ELNRNG\", \"EUNATCH\",\n    /*  50 */ \"ENOCSI\", \"EL2HLT\", \"EBADE\", \"EBADR\", \"EXFULL\", \"ENOANO\",\n    /*  56 */ \"EBADRQC\", \"EBADSLT\", \"\", \"EBFONT\", \"ENOSTR\", \"ENODATA\",\n    /*  62 */ \"ETIME\", \"ENOSR\", \"ENONET\", \"ENOPKG\", \"EREMOTE\",\n    /*  67 */ \"ENOLINK\", \"EADV\", \"ESRMNT\", \"ECOMM\", \"EPROTO\",\n    /*  72 */ \"EMULTIHOP\", \"EDOTDOT\", \"EBADMSG\", \"EOVERFLOW\",\n    /*  76 */ \"ENOTUNIQ\", \"EBADFD\", \"EREMCHG\", \"ELIBACC\", \"ELIBBAD\",\n    /*  81 */ \"ELIBSCN\", \"ELIBMAX\", \"ELIBEXEC\", \"EILSEQ\", \"ERESTART\",\n    /*  86 */ \"ESTRPIPE\", \"EUSERS\", \"ENOTSOCK\", \"EDESTADDRREQ\",\n    /*  90 */ \"EMSGSIZE\", \"EPROTOTYPE\", \"ENOPROTOOPT\",\n    /*  93 */ \"EPROTONOSUPPORT\", \"ESOCKTNOSUPPORT\",\n    /*  95 */ \"EOPNOTSUPP/ENOTSUP\", \"EPFNOSUPPORT\", \"EAFNOSUPPORT\",\n    /*  98 */ \"EADDRINUSE\", \"EADDRNOTAVAIL\", \"ENETDOWN\", \"ENETUNREACH\",\n    /* 102 */ \"ENETRESET\", \"ECONNABORTED\", \"ECONNRESET\", \"ENOBUFS\",\n    /* 106 */ \"EISCONN\", \"ENOTCONN\", \"ESHUTDOWN\", \"ETOOMANYREFS\",\n    /* 110 */ \"ETIMEDOUT\", \"ECONNREFUSED\", \"EHOSTDOWN\", \"EHOSTUNREACH\",\n    /* 114 */ \"EALREADY\", \"EINPROGRESS\", \"ESTALE\", \"EUCLEAN\",\n    /* 118 */ \"ENOTNAM\", \"ENAVAIL\", \"EISNAM\", \"EREMOTEIO\", \"EDQUOT\",\n    /* 123 */ \"ENOMEDIUM\", \"EMEDIUMTYPE\", \"ECANCELED\", \"ENOKEY\",\n    /* 127 */ \"EKEYEXPIRED\", \"EKEYREVOKED\", \"EKEYREJECTED\",\n    /* 130 */ \"EOWNERDEAD\", \"ENOTRECOVERABLE\", \"ERFKILL\", \"EHWPOISON\"\n};\n\n#define MAX_ENAME 133\n\nlong action_label_base, action_label, call_label_base, call_label, collapse_label_base, collapse_label;\n\nvoid output_error(bool use_err, const char *format, va_list ap)\n{\n  char text[BUF_SIZE], msg[BUF_SIZE], buf[BUF_SIZE];\n  vsnprintf(msg, BUF_SIZE, format, ap);\n  if (use_err)\n    snprintf(text, BUF_SIZE, \"[%s %s] \", 0 < errno && errno < MAX_ENAME ? ENAME[errno] : \"?UNKNOWN?\", strerror(errno));\n  else\n    strcpy(text, \"\");\n  snprintf(buf, BUF_SIZE, RED \"%s%s\\n\", text, msg);\n  fputs(buf, stderr);\n  fputs(SGR0, stderr);\n  fflush(stderr);\n}\n\nvoid err_msg(const char *format, ...)\n{\n  va_list ap;\n  va_start(ap, format);\n  int saved = errno;\n  output_error(errno > 0, format, ap);\n  errno = saved;\n  va_end(ap);\n}\n#define err_msg_g(...) ({err_msg(__VA_ARGS__); goto quit;})\n\nvoid err_exit(int exitno, const char *format, ...)\n{\n  va_list ap;\n  va_start(ap, format);\n  int saved = errno;\n  output_error(errno > 0, format, ap);\n  errno = saved;\n  va_end(ap);\n\n  void *bt[99];\n  char buf[1024];\n  int nptrs = backtrace(bt, LEN(buf));\n  int i = sprintf(buf, \"addr2line -Cfipe %s\", program_invocation_name), j = 0;\n  while (j < nptrs && i+30 < sizeof buf)\n    i += sprintf(buf+i, \" %p\", bt[j++]);\n  strcat(buf, \">&2\");\n  fputs(\"\\n\", stderr);\n  system(buf);\n  //backtrace_symbols_fd(buf, nptrs, STDERR_FILENO);\n  exit(exitno);\n}\n\nlong get_long(const char *arg)\n{\n  char *end;\n  errno = 0;\n  long ret = strtol(arg, &end, 0);\n  if (errno)\n    err_exit(EX_USAGE, \"get_long: %s\", arg);\n  if (*end)\n    err_exit(EX_USAGE, \"get_long: nonnumeric character\");\n  return ret;\n}\n\n//// log\n//\n\nvoid log_generic(const char *prefix, const char *format, va_list ap)\n{\n  char buf[BUF_SIZE];\n  timeval tv;\n  tm tm;\n  gettimeofday(&tv, NULL);\n  fputs(prefix, stdout);\n  if (localtime_r(&tv.tv_sec, &tm)) {\n    strftime(buf, sizeof buf, \"%T.%%06u \", &tm);\n    printf(buf, tv.tv_usec);\n  }\n  vprintf(format, ap);\n  fputs(SGR0, stdout);\n  fflush(stdout);\n}\n\nvoid log_event(const char *format, ...)\n{\n  va_list ap;\n  va_start(ap, format);\n  log_generic(CYAN, format, ap);\n  va_end(ap);\n}\n\nvoid log_action(const char *format, ...)\n{\n  va_list ap;\n  va_start(ap, format);\n  log_generic(GREEN, format, ap);\n  va_end(ap);\n}\n\nvoid log_status(const char *format, ...)\n{\n  va_list ap;\n  va_start(ap, format);\n  log_generic(YELLOW, format, ap);\n  va_end(ap);\n}\n\nvoid bold(long fd) { if (isatty(fd)) fputs(\"\\x1b[1m\", fd == STDOUT_FILENO ? stdout : stderr); }\nvoid blue(long fd) { if (isatty(fd)) fputs(BLUE, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid cyan(long fd) { if (isatty(fd)) fputs(CYAN, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid green(long fd) { if (isatty(fd)) fputs(GREEN, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid magenta(long fd) { if (isatty(fd)) fputs(MAGENTA, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid red(long fd) { if (isatty(fd)) fputs(RED, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid sgr0(long fd) { if (isatty(fd)) fputs(SGR0, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid yellow(long fd) { if (isatty(fd)) fputs(YELLOW, fd == STDOUT_FILENO ? stdout : stderr); }\nvoid normal_yellow(long fd) { if (isatty(fd)) fputs(NORMAL_YELLOW, fd == STDOUT_FILENO ? stdout : stderr); }\n\nvoid indent(FILE* f, int d)\n{\n  fprintf(f, \"%*s\", 2*d, \"\");\n}\n\nvoid DisjointIntervals::flip() {\n  long i = 0;\n  map<long, long> to2;\n  for (auto &x: to) {\n    if (i < x.first)\n      to2.emplace(i, x.first);\n    i = x.second;\n  }\n  if (i < AB)\n    to2.emplace(i, AB);\n  to = move(to2);\n}\n\nvoid DisjointIntervals::print() {\n  for (auto& x: to)\n    printf(\"(%ld,%ld) \", x.first, x.second);\n  puts(\"\");\n}\n"
  },
  {
    "path": "yanshi/src/common.hh",
    "content": "#pragma once\n#ifndef _GNU_SOURCE\n# define _GNU_SOURCE\n#endif\n#include <assert.h>\n#include <map>\n#include <stdarg.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <type_traits>\n#include <vector>\nusing std::map;\nusing std::vector;\n\ntypedef int8_t i8;\ntypedef int16_t i16;\ntypedef int32_t i32;\ntypedef int64_t i64;\ntypedef uint8_t u8;\ntypedef uint16_t u16;\ntypedef uint32_t u32;\ntypedef uint64_t u64;\ntypedef unsigned long ulong;\n\n#ifdef __APPLE__\n#include <crt_externs.h>\nextern char*** _NSGetArgv(void);\n#define program_invocation_name (((char **)*_NSGetArgv())[0])\n#define program_invocation_short_name (((char **)*_NSGetArgv())[0])\n#endif\n\n#define LEN(x) (sizeof(x)/sizeof(*x))\n#define ALL(x) (x).begin(), (x).end()\n#define REP(i, n) FOR(i, 0, n)\n#define FOR(i, a, b) for (typename std::remove_cv<typename std::remove_reference<decltype(b)>::type>::type i = (a); i < (b); i++)\n#define ROF(i, a, b) for (typename std::remove_cv<typename std::remove_reference<decltype(b)>::type>::type i = (b); --i >= (a); )\n\n#define SGR0 \"\\x1b[m\"\n#define RED \"\\x1b[1;31m\"\n#define GREEN \"\\x1b[1;32m\"\n#define YELLOW \"\\x1b[1;33m\"\n#define BLUE \"\\x1b[1;34m\"\n#define MAGENTA \"\\x1b[1;35m\"\n#define CYAN \"\\x1b[1;36m\"\n#define NORMAL_YELLOW \"\\x1b[33m\"\nconst long MAX_CODEPOINT = 0x10ffff;\nextern long action_label_base, action_label, call_label_base, call_label, collapse_label_base, collapse_label;\n\nvoid bold(long fd = 1);\nvoid blue(long fd = 1);\nvoid cyan(long fd = 1);\nvoid green(long fd = 1);\nvoid magenta(long fd = 1);\nvoid red(long fd = 1);\nvoid sgr0(long fd = 1);\nvoid yellow(long fd = 1);\nvoid normal_yellow(long fd = 1);\nvoid indent(FILE* f, int d);\n\nconst size_t BUF_SIZE = 512;\n\nvoid output_error(bool use_err, const char *format, va_list ap);\nvoid err_msg(const char *format, ...);\nvoid err_exit(int exitno, const char *format, ...);\n\nlong get_long(const char *arg);\n\nvoid log_generic(const char *prefix, const char *format, va_list ap);\nvoid log_event(const char *format, ...);\nvoid log_action(const char *format, ...);\nvoid log_status(const char *format, ...);\n\nextern long debug_level;\nextern FILE* debug_file;\n#define DP(level, ...)  do {           \\\n    if (level <= debug_level) {        \\\n      fprintf(debug_file, \"%s:%d \", __FILE__, __LINE__); \\\n      fprintf(debug_file, __VA_ARGS__);\\\n      fprintf(debug_file, \"\\n\");       \\\n      fflush(debug_file);              \\\n    }                                  \\\n  } while (0)\n\ntemplate<class T, class... Args>\nvoid emplace_front(vector<T>& a, Args&&... args)\n{\n  a.emplace(a.begin(), args...);\n}\n\ntemplate<class T>\nvoid sorted_insert(vector<T>& a, const T& x)\n{\n  a.emplace_back();\n  auto it = a.end();\n  while (a.begin() != --it && x < it[-1])\n    *it = it[-1];\n  *it = x;\n}\n\ntemplate<class T, class... Args>\nvoid sorted_emplace(vector<T>& a, Args&&... args)\n{\n  T x{args...};\n  a.emplace_back();\n  auto it = a.end();\n  while (a.begin() != --it && x < it[-1])\n    *it = it[-1];\n  *it = x;\n}\n\nstruct DisjointIntervals\n{\n  typedef std::pair<long, long> value_type;\n  std::map<long, long> to;\n  template<class... Args>\n  void emplace(Args&&... args) {\n    value_type x{args...};\n    auto it = to.lower_bound(x.first);\n    if (it != to.begin() && x.first <= prev(it)->second)\n      x.first = (--it)->first;\n    auto it2 = to.upper_bound(x.second);\n    if (it2 != to.begin() && prev(it2)->first <= x.second && x.second < prev(it2)->second)\n      x.second = prev(it2)->second;\n    while (it != it2)\n      it = to.erase(it);\n    to.emplace(x);\n  }\n  void flip();\n  void print(); // XXX\n};\n"
  },
  {
    "path": "yanshi/src/compiler.cc",
    "content": "#include \"compiler.hh\"\n#include \"fsa_anno.hh\"\n#include \"loader.hh\"\n#include \"option.hh\"\n\n#include <algorithm>\n#include <ctype.h>\n#include <limits.h>\n#include <map>\n#include <sstream>\n#include <stack>\n#include <unordered_map>\nusing namespace std;\n\nunordered_map<DefineStmt*, FsaAnno> compiled;\nstatic unordered_map<DefineStmt*, vector<pair<long, long>>> stmt2call_addr;\nstatic unordered_map<DefineStmt*, vector<bool>> stmt2final;\n\nvoid print_assoc(const FsaAnno& anno)\n{\n  magenta(); printf(\"=== Associated Expr of each state\\n\"); sgr0();\n  REP(i, anno.fsa.n()) {\n    printf(\"%ld:\", i);\n    for (auto aa: anno.assoc[i]) {\n      auto a = aa.first;\n      printf(\" %s%s%s%s(%ld-%ld\", a->name().c_str(),\n             has_start(aa.second) ? \"^\" : \"\",\n             has_inner(aa.second) ? \".\" : \"\",\n             has_final(aa.second) ? \"$\" : \"\",\n             a->loc.start, a->loc.end);\n      if (a->entering.size())\n        printf(\",>%zd\", a->entering.size());\n      if (a->leaving.size())\n        printf(\",%%%zd\", a->leaving.size());\n      if (a->finishing.size())\n        printf(\",@%zd\", a->finishing.size());\n      if (a->transiting.size())\n        printf(\",$%zd\", a->transiting.size());\n      printf(\")\");\n    }\n    puts(\"\");\n  }\n  puts(\"\");\n}\n\nvoid print_automaton(const Fsa& fsa)\n{\n  magenta(); printf(\"=== Automaton\\n\"); sgr0();\n  green(); printf(\"start: %ld\\n\", fsa.start);\n  red(); printf(\"finals:\");\n  for (long i: fsa.finals)\n    printf(\" %ld\", i);\n  puts(\"\");\n  sgr0(); puts(\"edges:\");\n  REP(i, fsa.n()) {\n    printf(\"%ld:\", i);\n    for (auto it = fsa.adj[i].begin(); it != fsa.adj[i].end(); ) {\n      long from = it->first.first, to = it->first.second, v = it->second;\n      while (++it != fsa.adj[i].end() && to == it->first.first && it->second == v)\n        to = it->first.second;\n      if (from == to-1)\n        printf(\" (%ld,%ld)\", from, v);\n      else\n        printf(\" (%ld-%ld,%ld)\", from, to-1, v);\n    }\n    puts(\"\");\n  }\n  puts(\"\");\n}\n\nExpr* find_lca(Expr* u, Expr* v)\n{\n  if (u->depth > v->depth)\n    swap(u, v);\n  if (u->depth < v->depth)\n    for (long k = 63-__builtin_clzl(v->depth-u->depth); k >= 0; k--)\n      if (u->depth <= v->depth-(1L<<k))\n        v = v->anc[k];\n  if (u == v)\n    return u;\n  if (v->depth)\n    for (long k = 63-__builtin_clzl(v->depth); k >= 0; k--)\n      if (k < u->anc.size() && u->anc[k] != v->anc[k])\n        u = u->anc[k], v = v->anc[k];\n  return u->anc[0]; // NULL if two trees\n}\n\nstruct Compiler : Visitor<Expr> {\n  stack<FsaAnno> st;\n  stack<Expr*> path;\n  long tick = 0;\n\n  void pre_expr(Expr& expr) {\n    expr.pre = tick++;\n    expr.depth = path.size();\n    if (path.size()) {\n      expr.anc.assign(1, path.top());\n      for (long k = 1; 1L << k <= expr.depth; k++)\n        expr.anc.push_back(expr.anc[k-1]->anc[k-1]);\n    } else\n      expr.anc.assign(1, nullptr);\n    path.push(&expr);\n    DP(5, \"%s(%ld-%ld)\", expr.name().c_str(), expr.loc.start, expr.loc.end);\n  }\n  void post_expr(Expr& expr) {\n    path.pop();\n    expr.post = tick;\n#ifdef DEBUG\n    st.top().fsa.check();\n#endif\n  }\n\n  void visit(Expr& expr) override {\n    pre_expr(expr);\n    expr.accept(*this);\n    post_expr(expr);\n  }\n  void visit(BracketExpr& expr) override {\n    st.push(FsaAnno::bracket(expr));\n  }\n  void visit(CallExpr& expr) override {\n    st.push(FsaAnno::call(expr));\n  }\n  void visit(CollapseExpr& expr) override {\n    st.push(FsaAnno::collapse(expr));\n  }\n  void visit(ComplementExpr& expr) override {\n    visit(*expr.inner);\n    st.top().complement(&expr);\n  }\n  void visit(ConcatExpr& expr) override {\n    visit(*expr.rhs);\n    FsaAnno rhs = move(st.top());\n    visit(*expr.lhs);\n    st.top().concat(rhs, &expr);\n  }\n  void visit(DifferenceExpr& expr) override {\n    visit(*expr.rhs);\n    FsaAnno rhs = move(st.top());\n    visit(*expr.lhs);\n    st.top().difference(rhs, &expr);\n  }\n  void visit(DotExpr& expr) override {\n    st.push(FsaAnno::dot(&expr));\n  }\n  void visit(EmbedExpr& expr) override {\n    st.push(FsaAnno::embed(expr));\n  }\n  void visit(EpsilonExpr& expr) override {\n    st.push(FsaAnno::epsilon_fsa(&expr));\n  }\n  void visit(IntersectExpr& expr) override {\n    visit(*expr.rhs);\n    FsaAnno rhs = move(st.top());\n    visit(*expr.lhs);\n    st.top().intersect(rhs, &expr);\n  }\n  void visit(LiteralExpr& expr) override {\n    st.push(FsaAnno::literal(expr));\n  }\n  void visit(PlusExpr& expr) override {\n    visit(*expr.inner);\n    st.top().plus(&expr);\n  }\n  void visit(QuestionExpr& expr) override {\n    visit(*expr.inner);\n    st.top().question(&expr);\n  }\n  void visit(RepeatExpr& expr) override {\n    visit(*expr.inner);\n    st.top().repeat(expr);\n  }\n  void visit(StarExpr& expr) override {\n    visit(*expr.inner);\n    st.top().star(&expr);\n  }\n  void visit(UnionExpr& expr) override {\n    visit(*expr.rhs);\n    FsaAnno rhs = move(st.top());\n    visit(*expr.lhs);\n    st.top().union_(rhs, &expr);\n  }\n};\n\nvoid compile(DefineStmt* stmt)\n{\n  if (compiled.count(stmt))\n    return;\n  FsaAnno& anno = compiled[stmt];\n  Compiler comp;\n  comp.visit(*stmt->rhs);\n  anno = move(comp.st.top());\n  anno.determinize(NULL, NULL);\n  anno.minimize(NULL);\n  DP(4, \"size(%s::%s) = %ld\", stmt->module->filename.c_str(), stmt->lhs.c_str(), anno.fsa.n());\n}\n\nvoid generate_transitions(DefineStmt* stmt)\n{\n  FsaAnno& anno = compiled[stmt];\n  auto& call_addr = stmt2call_addr[stmt];\n  auto& sub_final = stmt2final[stmt];\n  auto find_within = [&](long u) {\n    vector<pair<Expr*, ExprTag>> within;\n    Expr* last = NULL;\n    sort(ALL(anno.assoc[u]), [](const pair<Expr*, ExprTag>& x, const pair<Expr*, ExprTag>& y) {\n      if (x.first->pre != y.first->pre)\n        return x.first->pre < y.first->pre;\n      return x.second < y.second;\n    });\n    for (auto aa: anno.assoc[u]) {\n      Expr* stop = last ? find_lca(last, aa.first) : NULL;\n      last = aa.first;\n      for (Expr* x = aa.first; x != stop; x = x->anc[0])\n        within.emplace_back(x, aa.second);\n    }\n    sort(ALL(within));\n    auto j = within.begin();\n    for (auto i = within.begin(); i != within.end(); ) {\n      Expr* x = i->first;\n      long t = long(i->second);\n      while (++i != within.end() && x == i->first)\n        t |= long(i->second);\n      *j++ = {x, ExprTag(t)};\n    }\n    within.erase(j, within.end());\n    return within;\n  };\n  decltype(anno.assoc) withins(anno.fsa.n());\n  REP(i, anno.fsa.n())\n    withins[i] = move(find_within(i));\n\n  auto get_code = [](Action* action) {\n    if (auto t = dynamic_cast<InlineAction*>(action))\n      return t->code;\n    else if (auto t = dynamic_cast<RefAction*>(action))\n      return t->define_stmt->code;\n    else\n      assert(0);\n    return string();\n  };\n\n#define D(S) if (opt_dump_action) { \\\n               if (auto t = dynamic_cast<InlineAction*>(action.first)) { \\\n                 if (from == to-1) \\\n                   printf(S \" %ld %ld %ld %s\\n\", u, from, v, t->code.c_str()); \\\n                 else \\\n                   printf(S \" %ld %ld-%ld %ld %s\\n\", u, from, to-1, v, t->code.c_str()); \\\n               } else if (auto t = dynamic_cast<RefAction*>(action.first)) { \\\n                 if (from == to-1) \\\n                   printf(S \" %ld %ld %ld %s\\n\", u, from, v, t->define_stmt->code.c_str()); \\\n                 else \\\n                   printf(S \" %ld %ld-%ld %ld %s\\n\", u, from, to-1, v, t->define_stmt->code.c_str()); \\\n               } \\\n             }\n\n  if (output_header) {\n    if (opt_gen_c) {\n      if (opt_gen_extern_c)\n        fputs(\"extern \\\"C\\\" \", output_header);\n      fprintf(output_header, \"long yanshi_%s_transit(long* ret_stack, long* ret_stack_len, long u, long c\", stmt->lhs.c_str());\n    }\n    else\n      fprintf(output_header, \"long yanshi_%s_transit(vector<long>& ret_stack, long u, long c\", stmt->lhs.c_str());\n    if (stmt->export_params.size())\n      fprintf(output_header, \", %s\", stmt->export_params.c_str());\n    fprintf(output_header, \");\\n\");\n  }\n  if (opt_gen_c) {\n    if (opt_gen_extern_c)\n      fputs(\"extern \\\"C\\\" \", output);\n    fprintf(output, \"long yanshi_%s_transit(long* ret_stack, long* ret_stack_len, long u, long c\", stmt->lhs.c_str());\n  }\n  else\n    fprintf(output, \"long yanshi_%s_transit(vector<long>& ret_stack, long u, long c\", stmt->lhs.c_str());\n  if (stmt->export_params.size())\n    fprintf(output, \", %s\", stmt->export_params.c_str());\nfprintf(output,\n\")\\n\"\n\"{\\n\"\n\"  long v = -1;\\n\"\n\"again:\\n\"\n\"  switch (u) {\\n\");\n  REP(u, anno.fsa.n()) {\n    if (call_addr[u].first >= 0) { // no other transitions\n      fprintf(output,\n\"  case %ld:\\n\"\n\"    u = %ld;\\n\"\n, u, call_addr[u].first);\n      if (opt_gen_c)\n        fprintf(output,\n\"    if (*ret_stack_len >= %ld) return -1;\\n\"\n\"    ret_stack[(*ret_stack_len)++] = %ld;\\n\"\n, opt_max_return_stack, call_addr[u].second);\n      else\n        fprintf(output,\n\"    ret_stack.push_back(%ld);\\n\"\n, call_addr[u].second);\n      fprintf(output,\n\"    goto again;\\n\");\n      continue;\n    }\n    if (anno.fsa.adj[u].empty() && ! sub_final[u])\n      continue;\n    indent(output, 1);\n    fprintf(output, \"case %ld:\\n\", u);\n    indent(output, 2);\n    fprintf(output, \"switch (c) {\\n\");\n\n    unordered_map<long, pair<vector<pair<long, long>>, vector<pair<Action*, long>>>> v2case;\n    for (auto it = anno.fsa.adj[u].begin(); it != anno.fsa.adj[u].end(); ) {\n      long from = it->first.first, to = it->first.second, v = it->second;\n      while (++it != anno.fsa.adj[u].end() && to == it->first.first && it->second == v)\n        to = it->first.second;\n      v2case[v].first.emplace_back(from, to);\n      auto& body = v2case[v].second;\n\n      auto ie = withins[u].end(), je = withins[v].end();\n\n      // leaving = Expr(u) - Expr(v)\n      for (auto i = withins[u].begin(), j = withins[v].begin(); i != ie; ++i) {\n        while (j != je && i->first > j->first)\n          ++j;\n        if (j == je || i->first != j->first)\n          for (auto action: i->first->leaving) {\n            D(\"%%\");\n            body.push_back(action);\n          }\n      }\n\n      // entering = Expr(v) - Expr(u)\n      for (auto i = withins[u].begin(), j = withins[v].begin(); j != je; ++j) {\n        while (i != ie && i->first < j->first)\n          ++i;\n        if (i == ie || i->first != j->first)\n          for (auto action: j->first->entering) {\n            D(\">\");\n            body.push_back(action);\n          }\n      }\n\n      // transiting = intersect(Expr(u), Expr(v))\n      for (auto i = withins[u].begin(), j = withins[v].begin(); j != je; ++j) {\n        while (i != ie && i->first < j->first)\n          ++i;\n        if (i != ie && i->first == j->first)\n          for (auto action: j->first->transiting) {\n            D(\"$\");\n            body.push_back(action);\n          }\n      }\n\n      // finishing = intersect(Expr(u), Expr(v)) & Expr(v).has_final(v)\n      for (auto i = withins[u].begin(), j = withins[v].begin(); j != je; ++j) {\n        while (i != ie && i->first < j->first)\n          ++i;\n        if (i != ie && i->first == j->first && has_final(j->second))\n          for (auto action: j->first->finishing) {\n            D(\"@\");\n            body.push_back(action);\n          }\n      }\n    }\n\n    for (auto& x: v2case) {\n      for (auto& y: x.second.first) {\n        indent(output, 2);\n        if (y.first == y.second-1)\n          fprintf(output, \"case %ld:\\n\", y.first);\n        else\n          fprintf(output, \"case %ld ... %ld:\\n\", y.first, y.second-1);\n      }\n      indent(output, 3);\n      fprintf(output, \"v = %ld;\\n\", x.first);\n\n      // actions\n      sort(ALL(x.second.second), [](const pair<Action*, long>& a0, const pair<Action*, long>& a1) {\n        return a0.second != a1.second ? a0.second < a1.second : a0.first < a1.first;\n      });\n      x.second.second.erase(unique(ALL(x.second.second)), x.second.second.end());\n      for (auto a: x.second.second)\n        fprintf(output, \"{%s}\\n\", get_code(a.first).c_str());\n      indent(output, 3);\n      fprintf(output, \"break;\\n\");\n    }\n    // return from finals of DefineStmt called by CallExpr\n    if (sub_final[u]) {\n      indent(output, 2);\n      fprintf(output, \"default:\\n\");\n      indent(output, 3);\n      fprintf(output, opt_gen_c ?\n\"if (*ret_stack_len) { u = ret_stack[--*ret_stack_len]; goto again; }\\n\"\n:\n\"if (ret_stack.size()) { u = ret_stack.back(); ret_stack.pop_back(); goto again; }\\n\");\n      indent(output, 3);\n      fprintf(output, \"break;\\n\");\n    }\n\n    indent(output, 2);\n    fprintf(output, \"}\\n\");\n    indent(output, 2);\n    fprintf(output, \"break;\\n\");\n  }\n  indent(output, 1);\n  fprintf(output, \"}\\n\");\n  indent(output, 1);\n  fprintf(output, \"return v;\\n\");\n  fprintf(output, \"}\\n\\n\");\n}\n\nbool compile_export(DefineStmt* stmt)\n{\n  DP(2, \"Exporting %s\", stmt->lhs.c_str());\n  FsaAnno& anno = compiled[stmt];\n\n  DP(3, \"Construct automaton with all DefineStmt associated to referenced CallExpr/CollapseExpr\");\n  vector<vector<Edge>> adj;\n  decltype(anno.assoc) assoc;\n  vector<vector<DefineStmt*>> cllps;\n  long allo = 0;\n  unordered_map<DefineStmt*, long> stmt2offset;\n  unordered_map<DefineStmt*, long> stmt2start;\n  unordered_map<long, DefineStmt*> start2stmt;\n  vector<long> starts;\n  vector<bool> sub_final;\n  function<void(DefineStmt*)> allocate = [&](DefineStmt* stmt) {\n    if (stmt2offset.count(stmt))\n      return;\n    DP(4, \"Allocate %ld to %s\", allo, stmt->lhs.c_str());\n    FsaAnno& anno = compiled[stmt];\n    long base = stmt2offset[stmt] = allo;\n    allo += anno.fsa.n();\n    sub_final.resize(allo);\n    if (used_as_call.count(stmt)) {\n      stmt2start[stmt] = base+anno.fsa.start;\n      start2stmt[base+anno.fsa.start] = stmt;\n      starts.push_back(base+anno.fsa.start);\n      for (long f: anno.fsa.finals)\n        sub_final[base+f] = true;\n    }\n    adj.insert(adj.end(), ALL(anno.fsa.adj));\n    REP(i, anno.fsa.n())\n      for (auto& e: adj[base+i])\n        e.second += base;\n    assoc.insert(assoc.end(), ALL(anno.assoc));\n    FOR(i, base, base+anno.fsa.n()) {\n      for (auto aa: assoc[i])\n        if (has_start(aa.second)) {\n          if (auto* e = dynamic_cast<CallExpr*>(aa.first)) {\n            DefineStmt* v = e->define_stmt;\n            allocate(v);\n          } else if (auto* e = dynamic_cast<CollapseExpr*>(aa.first)) {\n            DefineStmt* v = e->define_stmt;\n            allocate(v);\n            // (i@{CollapseExpr,...}, special, _) -> ({CollapseExpr,...}, epsilon, CollapseExpr.define_stmt.start)\n            sorted_emplace(adj[i], epsilon, stmt2offset[v]+compiled[v].fsa.start);\n          }\n        }\n      long j = adj[i].size();\n      while (j && collapse_label_base < adj[i][j-1].first.second) {\n        long v = adj[i][j-1].second;\n        if (adj[i][j-1].first.first < collapse_label_base)\n          adj[i][j-1].first.second = collapse_label_base;\n        else\n          j--;\n        CollapseExpr* e;\n        for (auto aa: assoc[v])\n          if (has_final(aa.second) && (e = dynamic_cast<CollapseExpr*>(aa.first))) {\n            DefineStmt* w = e->define_stmt;\n            allocate(w);\n            // (_, special, v@{CollapseExpr,...}) -> (CollapseExpr.define_stmt.final, epsilon, v)\n            for (long f: compiled[w].fsa.finals) {\n              long g = stmt2offset[w]+f;\n              sorted_emplace(adj[g], epsilon, v);\n              if (g == i)\n                j++;\n            }\n          }\n      }\n      // remove (i, special, _)\n      adj[i].resize(j);\n    }\n  };\n  allocate(stmt);\n  anno.fsa.adj = move(adj);\n  anno.assoc = move(assoc);\n  anno.deterministic = false;\n  DP(3, \"# of states: %ld\", anno.fsa.n());\n\n  // substring grammar & this nonterminal is not marked as intact\n  if (opt_substring_grammar && ! stmt->intact) {\n    DP(3, \"Constructing substring grammar\");\n    anno.substring_grammar();\n    DP(3, \"# of states: %ld\", anno.fsa.n());\n  }\n\n  vector<vector<long>> map0;\n  DP(3, \"Determinize\");\n  anno.determinize(&starts, &map0);\n  vector<bool> sub_final2(anno.fsa.n());\n  REP(i, anno.fsa.n())\n    for (long u: map0[i]) {\n      if (sub_final[u])\n        sub_final2[i] = true;\n      if (start2stmt.count(u)) {\n        DefineStmt* stmt = start2stmt[u];\n        if (stmt2start[stmt] < 0) {\n          stmt->module->locfile.error_context(stmt->loc, \"the start has been included in multiple DFA states\");\n          return false;\n        }\n        stmt2start[stmt] = ~ i;\n      }\n    }\n  sub_final = move(sub_final2);\n  start2stmt.clear();\n  for (auto& it: stmt2start) {\n    it.second = ~ it.second;\n    start2stmt[it.second] = it.first;\n  }\n  DP(3, \"# of states: %ld\", anno.fsa.n());\n\n  DP(3, \"Minimize\");\n  map0.clear();\n  anno.minimize(&map0);\n  sub_final2.assign(anno.fsa.n(), false);\n  REP(i, anno.fsa.n())\n    for (long u: map0[i]) {\n      if (sub_final[u])\n        sub_final2[i] = true;\n      if (start2stmt.count(u)) {\n        DefineStmt* stmt = start2stmt[u];\n        stmt2start[stmt] = i;\n      }\n    }\n  sub_final = move(sub_final2);\n  start2stmt.clear();\n  for (auto& it: stmt2start)\n    start2stmt[it.second] = it.first;\n  DP(3, \"# of states: %ld\", anno.fsa.n());\n\n  if (! opt_keep_inaccessible) {\n    DP(3, \"Keep accessible states\");\n    // roots: start, starts of DefineStmt associated to CallExpr\n    starts.clear();\n    for (auto& it: stmt2start)\n      starts.push_back(it.second);\n    vector<long> map1;\n    anno.accessible(&starts, map1);\n    sub_final2.assign(anno.fsa.n(), false);\n    REP(i, anno.fsa.n()) {\n      long u = map1[i];\n      sub_final2[i] = sub_final[u];\n      if (start2stmt.count(u))\n        stmt2start[start2stmt[u]] = i;\n    }\n    sub_final = move(sub_final2);\n    start2stmt.clear();\n    for (auto& it: stmt2start)\n      start2stmt[it.second] = it.first;\n    DP(3, \"# of states: %ld\", anno.fsa.n());\n\n    DP(3, \"Keep co-accessible states\");\n    // roots: finals, finals of DefineStmt associated to CallExpr\n    map1.clear();\n    anno.co_accessible(&sub_final, map1);\n    sub_final2.assign(anno.fsa.n(), false);\n    REP(i, anno.fsa.n()) {\n      long u = map1[i];\n      sub_final2[i] = sub_final[u];\n      if (start2stmt.count(u))\n        stmt2start[start2stmt[u]] = i;\n    }\n    sub_final = move(sub_final2);\n    start2stmt.clear();\n    for (auto& it: stmt2start)\n      start2stmt[it.second] = it.first;\n    DP(3, \"# of states: %ld\", anno.fsa.n());\n  }\n\n  stmt2final[stmt] = sub_final;\n  auto& call_addr = stmt2call_addr[stmt];\n  call_addr.assign(anno.fsa.n(), make_pair(-1L, -1L));\n  DP(3, \"CallExpr\");\n  REP(i, anno.fsa.n())\n    if (anno.fsa.has_call(i)) {\n      if (anno.fsa.adj[i].size() != 1 || anno.fsa.adj[i][0].first.second-anno.fsa.adj[i][0].first.first > 1) {\n        stmt->module->locfile.error_context(stmt->loc, \"state %ld: CallExpr cannot coexist with other transitions\", i);\n        for (auto it = anno.fsa.adj[i].begin(); it != anno.fsa.adj[i].end(); ) {\n          long from = it->first.first, to = it->first.second, v = it->second;\n          while (++it != anno.fsa.adj[i].end() && to == it->first.first && it->second == v)\n            to = it->first.second;\n          fprintf(stderr, \"  (%ld,%ld)\\n\", from, to-1);\n        }\n        return false;\n      }\n      for (auto aa: anno.assoc[i])\n        if (has_start(aa.second))\n          if (auto* e = dynamic_cast<CallExpr*>(aa.first)) // unique\n            call_addr[i] = {stmt2start[e->define_stmt], anno.fsa.adj[i][0].second};\n    }\n\n  DP(3, \"Removing action/CallExpr labels\");\n  REP(i, anno.fsa.n()) {\n    long j = anno.fsa.adj[i].size();\n    while (j && action_label_base < anno.fsa.adj[i][j-1].first.second)\n      if (anno.fsa.adj[i][j-1].first.first < action_label_base)\n        anno.fsa.adj[i][j-1].first.second = action_label_base;\n      else\n        j--;\n    anno.fsa.adj[i].resize(j);\n  }\n\n  return true;\n}\n\n//// Graphviz dot renderer\n\nvoid generate_graphviz(Module* mo)\n{\n  fprintf(output, \"// Generated by 偃师, %s\\n\", mo->filename.c_str());\n  for (Stmt* x = mo->toplevel; x; x = x->next)\n    if (auto stmt = dynamic_cast<DefineStmt*>(x)) {\n      if (stmt->export_) {\n        FsaAnno& anno = compiled[stmt];\n\n        fprintf(output, \"digraph \\\"%s\\\" {\\n\", mo->filename.c_str());\n        bool start_is_final = false;\n\n        // finals\n        indent(output, 1);\n        fprintf(output, \"node[shape=doublecircle,color=olivedrab1,style=filled,fontname=Monospace];\");\n        for (long f: anno.fsa.finals)\n          if (f == anno.fsa.start)\n            start_is_final = true;\n          else\n            fprintf(output, \" %ld\", f);\n        fprintf(output, \"\\n\");\n\n        // start\n        indent(output, 1);\n        if (start_is_final)\n          fprintf(output, \"node[shape=doublecircle,color=orchid];\");\n        else\n          fprintf(output, \"node[shape=circle,color=orchid];\");\n        fprintf(output, \" %ld\\n\", anno.fsa.start);\n\n        // other states\n        indent(output, 1);\n        fprintf(output, \"node[shape=circle,color=black,style=\\\"\\\"]\\n\");\n\n        // edges\n        REP(u, anno.fsa.n()) {\n          unordered_map<long, stringstream> labels;\n          bool first = true;\n          auto it = anno.fsa.adj[u].begin();\n          for (; it != anno.fsa.adj[u].end(); ++it) {\n            stringstream& lb = labels[it->second];\n            if (! lb.str().empty())\n              lb << ',';\n            if (it->first.first == it->first.second-1)\n              lb << it->first.first;\n            else\n              lb << it->first.first << '-' << it->first.second-1;\n          }\n          for (auto& lb: labels) {\n            indent(output, 1);\n            fprintf(output, \"%ld -> %ld[label=\\\"%s\\\"]\\n\", u, lb.first, lb.second.str().c_str());\n          }\n        }\n      }\n    }\n  fprintf(output, \"}\\n\");\n}\n\n//// C++ renderer\n\nstatic void generate_final(const char* name, const vector<bool>& final)\n{\n  // comment\n  fprintf(output, \"  //static const long %sfinals[] = {\", name);\n  bool first = true;\n  REP(i, final.size())\n    if (final[i]) {\n      if (first) first = false;\n      else fprintf(output, \",\");\n      fprintf(output, \"%ld\", i);\n    }\n  fprintf(output, \"};\\n\");\n\n  first = true;\n  fprintf(output, \"  static const unsigned long %sfinal[] = {\", name);\n  for (long j = 0, i = 0; i < final.size(); i += CHAR_BIT*sizeof(long)) {\n      ulong mask = 0;\n      for (; j < final.size() && j < i+CHAR_BIT*sizeof(long); j++)\n        if (final[j]) {\n          mask |= 1uL << (j-i);\n        }\n      if (i) fprintf(output, \",\");\n      fprintf(output, \"%#lx\", mask);\n  }\n  fprintf(output, \"};\\n\");\n}\n\nstatic void generate_cxx_export(DefineStmt* stmt)\n{\n  FsaAnno& anno = compiled[stmt];\n\n  // yanshi_%s_init\n  if (output_header)\n    fprintf(output_header, \"extern long yanshi_%s_start;\\n\", stmt->lhs.c_str());\n  fprintf(output, \"long yanshi_%s_start = %ld;\\n\\n\", stmt->lhs.c_str(), anno.fsa.start);\n\n  // yanshi_%s_is_final\n  if (output_header) {\n    if (opt_gen_extern_c) fputs(\"extern \\\"C\\\" \", output_header);\n    fprintf(output_header, opt_gen_c ?\n\"bool yanshi_%s_is_final(const long* ret_stack, long ret_stack_len, long u);\\n\"\n:\n\"bool yanshi_%s_is_final(const vector<long>& ret_stack, long u);\\n\"\n, stmt->lhs.c_str());\n  }\n  if (opt_gen_extern_c) fputs(\"extern \\\"C\\\" \", output);\n  fprintf(output, opt_gen_c ?\n\"bool yanshi_%s_is_final(const long* ret_stack, long ret_stack_len, long u)\\n\"\n:\n\"bool yanshi_%s_is_final(const vector<long>& ret_stack, long u)\\n\"\n          , stmt->lhs.c_str());\n  fprintf(output, \"{\\n\");\n  vector<bool> final(anno.fsa.n());\n  for (long f: anno.fsa.finals)\n    final[f] = true;\n  generate_final(\"\", final);\n  generate_final(\"sub_\", stmt2final[stmt]);\n  fprintf(output, opt_gen_c ?\n\"  for (long i = ret_stack_len; i; u = ret_stack[--i])\\n\"\n:\n\"  for (auto i = ret_stack.size(); i; u = ret_stack[--i])\\n\"\n);\n  fprintf(output,\n\"    if (! (0 <= u && u < %ld && sub_final[u/(CHAR_BIT*sizeof(long))] >> (u%%(CHAR_BIT*sizeof(long))) & 1))\\n\"\n\"      return false;\\n\"\n\"  return 0 <= u && u < %ld && final[u/(CHAR_BIT*sizeof(long))] >> (u%%(CHAR_BIT*sizeof(long))) & 1;\\n\"\n\"};\\n\\n\"\n, anno.fsa.n() , anno.fsa.n()\n);\n  generate_transitions(stmt);\n}\n\nvoid generate_cxx(Module* mo)\n{\n  fprintf(output, \"// Generated by 偃师, %s\\n\", mo->filename.c_str());\n  fprintf(output, \"#include <limits.h>\\n\");\n  if (! opt_gen_c) {\n    fprintf(output, \"#include <vector>\\n\");\n    fprintf(output, \"using namespace std;\\n\");\n  } else {\n    fprintf(output, \"#include <stdbool.h>\\n\");\n  }\n  if (opt_standalone) {\n    fputs(\n\"#include <algorithm>\\n\"\n\"#include <cinttypes>\\n\"\n\"#include <clocale>\\n\"\n\"#include <codecvt>\\n\"\n\"#include <cstdint>\\n\"\n\"#include <cstdio>\\n\"\n\"#include <cstring>\\n\"\n\"#include <cwctype>\\n\"\n\"#include <iostream>\\n\"\n\"#include <locale>\\n\"\n\"#include <string>\\n\"\n\"using namespace std;\\n\"\n, output);\n  }\n  if (output_header) {\n    fputs(\"#pragma once\\n\", output_header);\n    if (! opt_gen_c) {\n      fprintf(output_header, \"#include <vector>\\n\");\n      fprintf(output_header, \"using std::vector;\\n\");\n    } else {\n      fprintf(output_header, \"#include <stdbool.h>\\n\");\n    }\n  }\n  fprintf(output, \"\\n\");\n  DefineStmt* main_export = NULL;\n  for (Stmt* x = mo->toplevel; x; x = x->next)\n    if (auto xx = dynamic_cast<DefineStmt*>(x)) {\n      if (xx->export_) {\n        if (! main_export)\n          main_export = xx;\n        generate_cxx_export(xx);\n      }\n    } else if (auto xx = dynamic_cast<CppStmt*>(x))\n      fprintf(output, \"%s\", xx->code.c_str());\n  if (opt_standalone && main_export) {\n    fprintf(output,\n\"\\n\"\n\"int main(int argc, char* argv[])\\n\"\n\"{\\n\"\n\"  setlocale(LC_ALL, \\\"\\\");\\n\"\n\"  string utf8;\\n\"\n\"  const char* p;\\n\"\n\"  long c, u = yanshi_%s_start, pref = 0;\\n\"\n, main_export->lhs.c_str());\n    if (opt_gen_c)\n      fprintf(output, \"  long ret_stack[%ld], ret_stack_len = 0;\\n\", opt_max_return_stack);\n    else\n      fprintf(output, \"  vector<long> ret_stack;\\n\");\n    fprintf(output,\n\"  if (argc == 2)\\n\"\n\"    utf8 = argv[1];\\n\"\n\"  else {\\n\"\n\"    FILE* f = argc == 1 ? stdin : fopen(argv[1], \\\"r\\\");\\n\"\n\"    while ((c = fgetc(f)) != EOF)\\n\"\n\"      utf8 += c;\\n\"\n\"    fclose(f);\\n\"\n\"  }\\n\"\n\"  u32string utf32 = wstring_convert<codecvt_utf8<char32_t>, char32_t>{}.from_bytes(utf8);\\n\"\n);\n    fprintf(output, opt_gen_c ?\n\"  printf(\\\"\\\\033[%%s33m%%ld \\\\033[m\\\", yanshi_%s_is_final(ret_stack, ret_stack_len, u) ? \\\"1;\\\" : \\\"\\\", u);\\n\"\n:\n\"  printf(\\\"\\\\033[%%s33m%%ld \\\\033[m\\\", yanshi_%s_is_final(ret_stack, u) ? \\\"1;\\\" : \\\"\\\", u);\\n\"\n, main_export->lhs.c_str()\n);\n    fprintf(output,\n\"  for (char32_t c: utf32) {\\n\");\n    fprintf(output, opt_gen_c ?\n\"    u = yanshi_%s_transit(ret_stack, &ret_stack_len, u, c);\\n\"\n:\n\"    u = yanshi_%s_transit(ret_stack, u, c);\\n\"\n, main_export->lhs.c_str());\n    fprintf(output,\n\"    if (c > WCHAR_MAX || iswcntrl(c)) printf(\\\"%%\\\" PRIuLEAST32 \\\" \\\", c);\\n\"\n\"    else cout << wstring_convert<codecvt_utf8<char32_t>, char32_t>{}.to_bytes(c) << ' ';\\n\");\n    fprintf(output, opt_gen_c ?\n\"    printf(\\\"\\\\033[%%s33m%%ld \\\\033[m\\\", yanshi_%s_is_final(ret_stack, ret_stack_len, u) ? \\\"1;\\\" : \\\"\\\", u);\\n\"\n:\n\"    printf(\\\"\\\\033[%%s33m%%ld \\\\033[m\\\", yanshi_%s_is_final(ret_stack, u) ? \\\"1;\\\" : \\\"\\\", u);\\n\"\n, main_export->lhs.c_str());\n    fprintf(output,\n\"    if (u < 0) break;\\n\"\n\"    pref++;\\n\"\n\"  }\\n\");\n    fprintf(output, opt_gen_c ?\n\"  printf(\\\"\\\\nlen: %%zd\\\\npref: %%ld\\\\nstate: %%ld\\\\nfinal: %%s\\\\n\\\", utf32.size(), pref, u, yanshi_%s_is_final(ret_stack, ret_stack_len, u) ? \\\"true\\\" : \\\"false\\\");\\n\"\n\"}\\n\"\n:\n\"  printf(\\\"\\\\nlen: %%zd\\\\npref: %%ld\\\\nstate: %%ld\\\\nfinal: %%s\\\\n\\\", utf32.size(), pref, u, yanshi_%s_is_final(ret_stack, u) ? \\\"true\\\" : \\\"false\\\");\\n\"\n\"}\\n\"\n, main_export->lhs.c_str());\n  }\n}\n"
  },
  {
    "path": "yanshi/src/compiler.hh",
    "content": "#pragma once\n#include \"fsa_anno.hh\"\n#include \"syntax.hh\"\n\n#include <unordered_map>\nusing std::unordered_map;\n\nvoid print_assoc(const FsaAnno& anno);\nvoid print_automaton(const Fsa& fsa);\nvoid compile(DefineStmt*);\nbool compile_export(DefineStmt* stmt);\nvoid generate_cxx(Module* mo);\nvoid generate_graphviz(Module* mo);\nextern unordered_map<DefineStmt*, FsaAnno> compiled;\n"
  },
  {
    "path": "yanshi/src/fsa.cc",
    "content": "#include \"common.hh\"\n#include \"fsa.hh\"\n#include \"option.hh\"\n\n#include <algorithm>\n#include <assert.h>\n#include <limits.h>\n#include <queue>\n#include <set>\n#include <stack>\n#include <tuple>\n#include <unordered_map>\n#include <unordered_set>\n#include <utility>\n#include <vector>\nusing namespace std;\n\nnamespace std\n{\n  template<typename T>\n  struct hash<vector<T>> {\n    size_t operator()(const vector<T>& v) const {\n      hash<T> h;\n      size_t r = 0;\n      for (auto x: v)\n        r = r*17+h(x);\n      return r;\n    }\n  };\n}\n\nvoid Fsa::check() const\n{\n  REP(i, n())\n    FOR(j, 1, adj[i].size())\n      assert(adj[i][j-1].first.second == 0 &&\n             adj[i][j].first.second == 0 ||\n             adj[i][j-1].first.second <= adj[i][j].first.first);\n}\n\nbool Fsa::has(long u, long c) const\n{\n  auto it = upper_bound(ALL(adj[u]), make_pair(make_pair(c, LONG_MAX), LONG_MAX));\n  return it != adj[u].begin() && c < (--it)->first.second;\n}\n\nbool Fsa::has_call(long u) const\n{\n  auto it = upper_bound(ALL(adj[u]), make_pair(make_pair(call_label_base, LONG_MAX), LONG_MAX));\n  return (it != adj[u].end() && it->first.first < call_label) || (it != adj[u].begin() && call_label_base < (--it)->first.second);\n}\n\nbool Fsa::has_call_or_collapse(long u) const\n{\n  auto it = upper_bound(ALL(adj[u]), make_pair(make_pair(call_label_base, LONG_MAX), LONG_MAX));\n  return it != adj[u].end() || (it != adj[u].begin() && call_label_base < (--it)->first.second);\n}\n\nlong Fsa::transit(long u, long c) const\n{\n  auto it = upper_bound(ALL(adj[u]), make_pair(make_pair(c, LONG_MAX), LONG_MAX));\n  return it != adj[u].begin() && c < (--it)->first.second ? it->second : -1;\n}\n\nbool Fsa::is_final(long x) const\n{\n  return binary_search(ALL(finals), x);\n}\n\nvoid Fsa::epsilon_closure(vector<long>& src) const\n{\n  static vector<bool> vis;\n  if (n() > vis.size())\n    vis.resize(n());\n  for (long i: src)\n    vis[i] = true;\n  REP(i, src.size()) {\n    long u = src[i];\n    for (auto& e: adj[u]) {\n      if (-1 < e.first.first) break;\n      if (! vis[e.second]) {\n        vis[e.second] = true;\n        src.push_back(e.second);\n      }\n    }\n  }\n  for (long i: src)\n    vis[i] = false;\n  sort(ALL(src));\n}\n\nFsa Fsa::operator~() const\n{\n  long accept = n();\n  Fsa r;\n  r.start = start;\n  r.adj.resize(accept+1);\n  REP(i, accept) {\n    long j = 0;\n    for (auto& e: adj[i]) {\n      if (j < e.first.first)\n        r.adj[i].emplace_back(make_pair(j, e.first.first), accept);\n      r.adj[i].emplace_back(e.first, e.second);\n      j = e.first.second;\n    }\n    if (j < AB)\n      r.adj[i].emplace_back(make_pair(j, AB), accept);\n  }\n  r.adj[accept].emplace_back(make_pair(0, AB), accept);\n  vector<long> new_finals;\n  auto j = finals.begin();\n  REP(i, accept+1) {\n    while (j != finals.end() && *j < i)\n      ++j;\n    if (j == finals.end() || *j != i)\n      new_finals.push_back(i);\n  }\n  r.finals = move(new_finals);\n  return r;\n}\n\nvoid Fsa::accessible(const vector<long>* starts, function<void(long)> relate)\n{\n  vector<long> q{start}, id(n(), 0);\n  id[start] = 1;\n  if (starts)\n    for (long u: *starts)\n      if (! id[u]) {\n        id[u] = 1;\n        q.push_back(u);\n      }\n  REP(i, q.size()) {\n    long u = q[i];\n    for (auto& e: adj[u]) {\n      //if (e.first.first >= AB) break;\n      if (! id[e.second]) {\n        id[e.second] = 1;\n        q.push_back(e.second);\n      }\n    }\n  }\n\n  long j = 0;\n  REP(i, n())\n    id[i] = id[i] ? j++ : -1;\n\n  auto it = finals.begin(), it2 = it;\n  REP(i, n())\n    if (id[i] >= 0) {\n      relate(i);\n      if (start == i)\n        start = id[i];\n      while (it != finals.end() && *it < i)\n        ++it;\n      if (it != finals.end() && *it == i)\n        *it2++ = id[i];\n      long k = 0;\n      for (auto& e: adj[i])\n        if (id[e.second] >= 0)\n          adj[i][k++] = {e.first, id[e.second]}; // unordered unless deterministic\n      adj[i].resize(k);\n      if (id[i] != i)\n        adj[id[i]] = move(adj[i]);\n    }\n  finals.erase(it2, finals.end());\n  adj.resize(j);\n}\n\nvoid Fsa::co_accessible(const vector<bool>*final, function<void(long)> relate)\n{\n  vector<vector<long>> radj(n());\n  REP(i, n())\n    for (auto& e: adj[i]) {\n      //if (e.first.first >= AB) break;\n      radj[e.second].push_back(i);\n    }\n  REP(i, n())\n    sort(ALL(radj[i]));\n  vector<long> q = finals, id(n(), 0);\n  for (long f: finals)\n    id[f] = 1;\n  if (final)\n    REP(i, n())\n      if ((*final)[i] && ! id[i]) {\n        id[i] = 1;\n        q.push_back(i);\n      }\n  REP(i, q.size()) {\n    long u = q[i];\n    for (auto& v: radj[u])\n      if (! id[v]) {\n        id[v] = 1;\n        q.push_back(v);\n      }\n  }\n  if (! id[start]) {\n    start = 0;\n    finals.clear();\n    adj.assign(1, {});\n    return;\n  }\n\n  long j = 0;\n  REP(i, n())\n    id[i] = id[i] ? j++ : -1;\n\n  auto it = finals.begin(), it2 = it;\n  REP(i, n())\n    if (id[i] >= 0) {\n      relate(i);\n      if (start == i)\n        start = id[i];\n      while (it != finals.end() && *it < i)\n        ++it;\n      if (it != finals.end() && *it == i)\n        *it2++ = id[i];\n      long k = 0;\n      for (auto& e: adj[i])\n        if (id[e.second] >= 0)\n          adj[i][k++] = {e.first, id[e.second]}; // unordered unless deterministic\n      adj[i].resize(k);\n      if (id[i] != i)\n        adj[id[i]] = move(adj[i]);\n    }\n  finals.erase(it2, finals.end());\n  adj.resize(j);\n}\n\nFsa Fsa::difference(const Fsa& rhs, function<void(long)> relate) const\n{\n  Fsa r;\n  vector<pair<long, long>> q;\n  unordered_map<long, long> m;\n  q.emplace_back(start, rhs.start);\n  m[(rhs.n()+1) * start + rhs.start] = 0;\n  r.start = 0;\n  REP(i, q.size()) {\n    long u0, u1, v0, v1;\n    tie(u0, u1) = q[i];\n    if (is_final(u0) && ! rhs.is_final(u1))\n      r.finals.push_back(i);\n    r.adj.emplace_back();\n    relate(u0);\n    vector<Edge>::const_iterator it0 = adj[u0].begin(), it1, it1e;\n    if (u1 == rhs.n())\n      it1 = it1e = rhs.adj[0].end();\n    else {\n      it1 = rhs.adj[u1].begin();\n      it1e = rhs.adj[u1].end();\n    }\n    long last = LONG_MIN;\n    while (it0 != adj[u0].end()) {\n      long from = max(last, it0->first.first), to = it0->first.second;\n      while (it1 != it1e && it1->first.second <= from)\n        ++it1;\n      if (it1 != it1e)\n        to = min(to, from < it1->first.first ? it1->first.first : it1->first.second);\n      last = to;\n      long v1 = it1 != it1e && it1->first.first <= from ? it1->second : rhs.n(),\n           t = (rhs.n()+1) * it0->second + v1;\n      auto mit = m.find(t);\n      if (mit == m.end()) {\n        mit = m.emplace(t, m.size()).first;\n        q.emplace_back(it0->second, v1);\n      }\n      r.adj[i].emplace_back(make_pair(from, to), mit->second);\n      if (to == it0->first.second)\n        ++it0;\n    }\n  }\n  return r;\n}\n\nFsa Fsa::intersect(const Fsa& rhs, function<void(long, long)> relate) const\n{\n  Fsa r;\n  vector<pair<long, long>> q;\n  long u0, u1, v0, v1;\n  unordered_map<long, long> m;\n  q.emplace_back(start, rhs.start);\n  m[rhs.n() * start + rhs.start] = 0;\n  r.start = 0;\n  REP(i, q.size()) {\n    tie(u0, u1) = q[i];\n    if (is_final(u0) && rhs.is_final(u1))\n      r.finals.push_back(i);\n    r.adj.emplace_back();\n    relate(u0, u1);\n    auto it0 = adj[u0].begin(), it1 = rhs.adj[u1].begin();\n    while (it0 != adj[u0].end() && it1 != rhs.adj[u1].end()) {\n      if (it0->first.second <= it1->first.first)\n        ++it0;\n      else if (it1->first.second <= it0->first.first)\n        ++it1;\n      else {\n        long t = rhs.n() * it0->second + it1->second;\n        auto mit = m.find(t);\n        if (mit == m.end()) {\n          mit = m.emplace(t, m.size()).first;\n          q.emplace_back(it0->second, it1->second);\n        }\n        r.adj[i].emplace_back(make_pair(max(it0->first.first, it1->first.first), min(it0->first.second, it1->first.second)), mit->second);\n        if (it0->first.second < it1->first.second)\n          ++it0;\n        else if (it0->first.second > it1->first.second)\n          ++it1;\n        else\n          ++it0, ++it1;\n      }\n    }\n  }\n  return r;\n}\n\nFsa Fsa::determinize(const vector<long>* starts, function<void(long, const vector<long>&)> relate) const\n{\n  Fsa r;\n  r.start = 0;\n  unordered_map<vector<long>, long> m;\n  vector<vector<Edge>::const_iterator> its(n());\n  vector<long> vs{start};\n  vector<pair<long, long>> events;\n  stack<vector<long>> st;\n  epsilon_closure(vs);\n  m[vs] = 0;\n  st.push(move(vs));\n  if (starts)\n    for (long u: *starts) {\n      vs.assign(1, u);\n      epsilon_closure(vs);\n      if (! m.count(vs)) {\n        m.emplace(vs, m.size());\n        st.push(move(vs));\n      }\n    }\n  while (st.size()) {\n    vector<long> x = move(st.top());\n    st.pop();\n    long id = m[x];\n    if (id+1 > r.adj.size())\n      r.adj.resize(id+1);\n    relate(id, x);\n    bool final = false;\n    events.clear();\n    for (long u: x) {\n      if (is_final(u))\n        final = true;\n      for (auto& e: adj[u]) {\n        events.emplace_back(e.first.first, e.second);\n        events.emplace_back(e.first.second, ~ e.second);\n      }\n    }\n    if (final)\n      r.finals.push_back(id);\n    long last = 0;\n    multiset<long> live;\n    sort(ALL(events));\n    for (auto& ev: events) {\n      if (last < ev.first) {\n        if (live.size()) {\n          vs.assign(ALL(live));\n          vs.erase(unique(ALL(vs)), vs.end());\n          epsilon_closure(vs);\n          auto mit = m.find(vs);\n          if (mit == m.end()) {\n            mit = m.emplace(vs, m.size()).first;\n            st.push(vs);\n          }\n          if (r.adj[id].size() && r.adj[id].back().first.second == last && r.adj[id].back().second == mit->second) // coalesce two edges\n            r.adj[id].back().first.second = ev.first;\n          else\n            r.adj[id].emplace_back(make_pair(last, ev.first), mit->second);\n        }\n        last = ev.first;\n      }\n      if (ev.second >= 0)\n        live.insert(ev.second);\n      else\n        live.erase(live.find(~ ev.second));\n    }\n  }\n  sort(ALL(r.finals));\n  return r;\n}\n\nFsa Fsa::distinguish(function<void(vector<long>&)> relate) const\n{\n  vector<long> scale;\n  REP(i, n())\n    for (auto& e: adj[i]) {\n      scale.push_back(e.first.first);\n      scale.push_back(e.first.second);\n    }\n  sort(ALL(scale));\n  scale.erase(unique(ALL(scale)), scale.end());\n\n  vector<vector<pair<long, long>>> radj(n());\n  REP(i, n())\n    for (auto& e: adj[i]) {\n      long from = lower_bound(ALL(scale), e.first.first) - scale.begin(),\n           to = lower_bound(ALL(scale), e.first.second) - scale.begin();\n      FOR(j, from, to)\n        radj[e.second].emplace_back(j, i);\n    }\n  REP(i, n())\n    sort(ALL(radj[i]));\n  vector<long> L(n()), R(n()), B(n()), C(n(), 0), CC(n(), 0);\n  vector<bool> mark(n(), false);\n\n  // distinguish finals & non-finals\n  long fx = -1, x = -1, fy = -1, y = -1, j = 0;\n  REP(i, n())\n    if (j < finals.size() && finals[j] == i) {\n      j++;\n      if (y < 0)\n        fy = i;\n      else\n        R[y] = i;\n      C[B[i] = fy]++;\n      L[i] = y;\n      y = i;\n    } else {\n      if (x < 0)\n        fx = i;\n      else\n        R[x] = i;\n      C[B[i] = fx]++;\n      L[i] = x;\n      x = i;\n    }\n  if (x >= 0)\n    L[fx] = x, R[x] = fx;\n  if (y >= 0)\n    L[fy] = y, R[y] = fy;\n\n  set<pair<long, long>> refines;\n  auto labels = [&](long fx) {\n    vector<long> lb;\n    for (long x = fx; ; ) {\n      for (auto& e: radj[x])\n        lb.push_back(e.first);\n      if ((x = R[x]) == fx) break;\n    }\n    sort(ALL(lb));\n    lb.erase(unique(ALL(lb)), lb.end());\n    return lb;\n  };\n\n  if (fx >= 0)\n    for (long a: labels(fx))\n      refines.emplace(a, fx);\n  if (fy >= 0)\n    for (long a: labels(fy))\n      refines.emplace(a, fy);\n  while (refines.size()) {\n    long a;\n    tie(a, fx) = *refines.begin();\n    refines.erase(refines.begin());\n    // count\n    vector<long> bs;\n    for (x = fx; ; ) {\n      auto it = lower_bound(ALL(radj[x]), make_pair(a, 0L)),\n           ite = upper_bound(ALL(radj[x]), make_pair(a, n()));\n      for (; it != ite; ++it) {\n        y = it->second;\n        if (! CC[B[y]]++)\n          bs.push_back(B[y]);\n        mark[y] = true;\n      }\n      if ((x = R[x]) == fx) break;\n    }\n    // for each refinable set\n    for (long fy: bs) {\n      if (CC[fy] < C[fy]) {\n        long fu = -1, u = -1, cu = 0,\n             fv = -1, v = -1, cv = 0;\n        vector<long> lb = labels(fy);\n        for (long i = fy; ; ) {\n          if (mark[i]) {\n            mark[i] = false;\n            if (u < 0)\n              C[fu = i] = 0;\n            else\n              R[u] = i;\n            C[fu]++;\n            B[i] = fu;\n            L[i] = u;\n            u = i;\n          } else {\n            if (v < 0)\n              C[fv = i] = 0;\n            else\n              R[v] = i;\n            C[fv]++;\n            B[i] = fv;\n            L[i] = v;\n            v = i;\n          }\n          if ((i = R[i]) == fy) break;\n        }\n        L[fu] = u, R[u] = fu;\n        L[fv] = v, R[v] = fv;\n        //REP(a, AB+1)\n        for (long a: lb)\n          if (refines.count({a, fy}))\n            refines.emplace(a, fu != fy ? fu : fv);\n          else\n            refines.emplace(a, C[fu] < C[fv] ? fu : fv);\n      } else\n        for (long i = fy; ; ) {\n          mark[i] = false;\n          if ((i = R[i]) == fy) break;\n        }\n      CC[fy] = 0;\n    }\n    // clear marks\n    for (x = fx; ; ) {\n      auto it = lower_bound(ALL(radj[x]), make_pair(a, 0L)),\n           ite = upper_bound(ALL(radj[x]), make_pair(a, n()));\n      for (; it != ite; ++it) {\n        y = it->second;\n        CC[B[y]] = 0;\n        mark[y] = false;\n      }\n      if ((x = R[x]) == fx) break;\n    }\n  }\n\n  Fsa r;\n  long nn = 0;\n  vector<long> vs;\n  REP(i, n())\n    if (B[i] == i) {\n      vs.clear();\n      for (long j = i; ; ) {\n        B[j] = nn;\n        vs.push_back(j);\n        if ((j = R[j]) == i) break;\n      }\n      relate(vs);\n      if (binary_search(ALL(finals), i))\n        r.finals.push_back(nn);\n      nn++;\n    }\n  r.start = B[start];\n  r.adj.resize(nn);\n  REP(i, n())\n    for (auto& e: adj[i])\n      r.adj[B[i]].emplace_back(e.first, B[e.second]);\n  REP(i, nn) {\n    // merge edges with the same destination\n    sort(ALL(r.adj[i]), [](const Edge& x, const Edge& y) {\n      return x.second != y.second ? x.second < y.second : x.first < y.first;\n    });\n    auto it2 = r.adj[i].begin();\n    for (auto it = r.adj[i].begin(); it != r.adj[i].end(); ) {\n      long v = it->second, from = it->first.first, to = it->first.second;\n      while (++it != r.adj[i].end() && it->second == v)\n        if (it->first.first <= to)\n          to = max(to, it->first.second);\n        else {\n          *it2++ = make_pair(make_pair(from, to), v);\n          tie(from, to) = it->first;\n        }\n      *it2++ = make_pair(make_pair(from, to), v);\n    }\n    r.adj[i].erase(it2, r.adj[i].end());\n    sort(ALL(r.adj[i]));\n  }\n  return r;\n}\n"
  },
  {
    "path": "yanshi/src/fsa.hh",
    "content": "#pragma once\n#include <functional>\n#include <utility>\n#include <vector>\nusing std::function;\nusing std::pair;\nusing std::vector;\n\ntypedef pair<long, long> Label;\ntypedef pair<Label, long> Edge;\n\nconst Label epsilon{-1L, 0L};\n\nstruct Fsa {\n  long start;\n  vector<long> finals; // sorted\n  vector<vector<Edge>> adj; // sorted\n\n  void check() const;\n  long n() const { return adj.size(); }\n  bool is_final(long x) const;\n  bool has(long u, long c) const;\n  bool has_call(long u) const;\n  bool has_call_or_collapse(long u) const;\n  long transit(long u, long c) const;\n  void epsilon_closure(vector<long>& src) const;\n  Fsa operator~() const;\n  // a -> a\n  void accessible(const vector<long>* starts, function<void(long)> relate);\n  // a -> a\n  void co_accessible(const vector<bool>* final, function<void(long)> relate);\n  // DFA -> DFA -> DFA\n  Fsa intersect(const Fsa& rhs, function<void(long, long)> relate) const;\n  // DFA -> DFA -> DFA\n  Fsa difference(const Fsa& rhs, function<void(long)> relate) const;\n  // DFA -> DFA\n  Fsa distinguish(function<void(vector<long>&)> relate) const;\n  // * -> DFA\n  Fsa determinize(const vector<long>* starts, function<void(long, const vector<long>&)> relate) const;\n};\n"
  },
  {
    "path": "yanshi/src/fsa_anno.cc",
    "content": "#include \"common.hh\"\n#include \"compiler.hh\"\n#include \"fsa_anno.hh\"\n#include \"loader.hh\"\n#include \"option.hh\"\n\n#include <algorithm>\n#include <limits.h>\n#include <map>\n#include <unicode/utf8.h>\n#include <utility>\nusing namespace std;\n\nbool operator<(ExprTag x, ExprTag y)\n{\n  return long(x) < long(y);\n}\n\nbool assoc_has_expr(vector<pair<Expr*, ExprTag>>& as, Expr* x)\n{\n  auto it = lower_bound(ALL(as), make_pair(x, ExprTag(0)));\n  return it != as.end() && it->first == x;\n}\n\nvoid sort_assoc(vector<pair<Expr*, ExprTag>>& as)\n{\n  sort(ALL(as));\n  auto i = as.begin(), j = i, k = i;\n  for (; i != as.end(); i = j) {\n    while (++j != as.end() && i->first == j->first)\n      i->second = ExprTag(long(i->second) | long(j->second));\n    *k++ = *i;\n  }\n  as.erase(k, as.end());\n}\n\nvoid FsaAnno::add_assoc(Expr& expr)\n{\n  // has actions: actions need tags to differentiate 'entering', 'leaving', ...\n  // 'intact': states with the 'inner' tag cannot be connected to start/final in substring grammar\n  // 'CallExpr' 'CollapseExpr': differentiate states representing 'CallExpr' 'CollapseExpr' (u, special, v)\n  // 'opt_mode': displaying possible positions for given strings in interactive mode\n  if (expr.no_action() && ! expr.stmt->intact && ! dynamic_cast<CallExpr*>(&expr) && ! dynamic_cast<CollapseExpr*>(&expr) && opt_mode != Mode::interactive)\n    return;\n  auto j = fsa.finals.begin();\n  REP(i, fsa.n()) {\n    ExprTag tag = ExprTag(0);\n    if (i == fsa.start)\n      tag = ExprTag::start;\n    while (j != fsa.finals.end() && *j < i)\n      ++j;\n    if (j != fsa.finals.end() && *j == i)\n      tag = ExprTag(long(tag) | long(ExprTag::final));\n    if (tag == ExprTag(0))\n      tag = ExprTag::inner;\n    sorted_insert(assoc[i], make_pair(&expr, tag));\n  }\n  // Add pseudo transitions with labels [ACTION_LABEL_BASE, COLLAPSE_LABEL_BASE) to prevent its merge with other states\n  if (expr.leaving.size() || expr.entering.size() || expr.transiting.size())\n    for (auto action: expr.transiting)\n      REP(i, fsa.n()) {\n        fsa.adj[i].emplace_back(make_pair(action_label, action_label+1), i);\n        action_label++;\n      }\n  else if (expr.finishing.size())\n    for (long f: fsa.finals) {\n      fsa.adj[f].emplace_back(make_pair(action_label, action_label+1), f);\n      action_label++;\n    }\n}\n\nvoid FsaAnno::accessible(const vector<long>* starts, vector<long>& mapping) {\n  long allo = 0;\n  auto relate = [&](long x) {\n    if (allo != x)\n      assoc[allo] = move(assoc[x]);\n    allo++;\n    mapping.push_back(x);\n  };\n  fsa.accessible(starts, relate);\n  assoc.resize(allo);\n}\n\nvoid FsaAnno::co_accessible(const vector<bool>* final, vector<long>& mapping) {\n  long allo = 0;\n  auto relate = [&](long x) {\n    if (allo != x)\n      assoc[allo] = move(assoc[x]);\n    allo++;\n    mapping.push_back(x);\n  };\n  fsa.co_accessible(final, relate);\n  if (fsa.finals.empty()) { // 'start' does not produce acceptable strings\n    assoc.assign(1, {});\n    mapping.assign(1, 0);\n    deterministic = true;\n    return;\n  }\n  if (! deterministic)\n    REP(i, fsa.n())\n      sort(ALL(fsa.adj[i]));\n  assoc.resize(allo);\n}\n\nvoid FsaAnno::complement(ComplementExpr* expr) {\n  if (! deterministic)\n    fsa = fsa.determinize(NULL, [&](long, const vector<long>&){});\n  fsa = ~ fsa;\n  assoc.assign(fsa.n(), {});\n  deterministic = true;\n}\n\nvoid FsaAnno::concat(FsaAnno& rhs, ConcatExpr* expr) {\n  long ln = fsa.n(), rn = rhs.fsa.n();\n  for (long f: fsa.finals)\n    emplace_front(fsa.adj[f], epsilon, ln+rhs.fsa.start);\n  for (auto& es: rhs.fsa.adj) {\n    for (auto& e: es)\n      e.second += ln;\n    fsa.adj.emplace_back(move(es));\n  }\n  fsa.finals = move(rhs.fsa.finals);\n  for (long& f: fsa.finals)\n    f += ln;\n  assoc.resize(fsa.n());\n  REP(i, rhs.fsa.n())\n    assoc[ln+i] = move(rhs.assoc[i]);\n  if (expr)\n    add_assoc(*expr);\n  deterministic = false;\n}\n\nvoid FsaAnno::determinize(const vector<long>* starts, vector<vector<long>>* mapping) {\n  if (deterministic)\n    return;\n  decltype(assoc) new_assoc;\n  auto relate = [&](long id, const vector<long>& xs) {\n    if (id+1 > new_assoc.size()) {\n      new_assoc.resize(id+1);\n      if (mapping)\n        mapping->resize(id+1);\n    }\n    auto& as = new_assoc[id];\n    for (long x: xs)\n      as.insert(as.end(), ALL(assoc[x]));\n    sort_assoc(as);\n    if (mapping)\n      (*mapping)[id] = xs;\n  };\n  fsa = fsa.determinize(starts, relate);\n  assoc = move(new_assoc);\n  deterministic = true;\n}\n\nvoid FsaAnno::difference(FsaAnno& rhs, DifferenceExpr* expr) {\n  vector<vector<long>> rel0;\n  decltype(rhs.assoc) new_assoc;\n  auto relate0 = [&](long id, const vector<long>& xs) {\n    if (id+1 > rel0.size())\n      rel0.resize(id+1);\n    rel0[id] = xs;\n  };\n  auto relate = [&](long x) {\n    if (rel0.empty())\n      new_assoc.emplace_back(assoc[x]);\n    else {\n      new_assoc.emplace_back();\n      auto& as = new_assoc.back();\n      for (long u: rel0[x])\n        as.insert(as.end(), ALL(assoc[u]));\n      sort_assoc(as);\n    }\n  };\n  if (! deterministic)\n    fsa = fsa.determinize(NULL, relate0);\n  if (! rhs.deterministic)\n    rhs.fsa = rhs.fsa.determinize(NULL, [](long, const vector<long>&) {});\n  fsa = fsa.difference(rhs.fsa, relate);\n  assoc = move(new_assoc);\n  if (expr)\n    add_assoc(*expr);\n  deterministic = true;\n}\n\nFsaAnno FsaAnno::epsilon_fsa(EpsilonExpr* expr) {\n  FsaAnno r;\n  r.fsa.start = 0;\n  r.fsa.finals.push_back(0);\n  r.fsa.adj.resize(1);\n  r.assoc.resize(1);\n  if (expr)\n    r.add_assoc(*expr);\n  r.deterministic = true;\n  return r;\n}\n\nvoid FsaAnno::intersect(FsaAnno& rhs, IntersectExpr* expr) {\n  decltype(rhs.assoc) new_assoc;\n  vector<vector<long>> rel0, rel1;\n  auto relate0 = [&](long id, const vector<long>& xs) {\n    if (id+1 > rel0.size())\n      rel0.resize(id+1);\n    rel0[id] = xs;\n  };\n  auto relate1 = [&](long id, const vector<long>& xs) {\n    if (id+1 > rel1.size())\n      rel1.resize(id+1);\n    rel1[id] = xs;\n  };\n  auto relate = [&](long x, long y) {\n    new_assoc.emplace_back();\n    auto& as = new_assoc.back();\n    if (rel0.empty())\n      as.insert(as.end(), ALL(assoc[x]));\n    else\n      for (long u: rel0[x])\n        as.insert(as.end(), ALL(assoc[u]));\n    if (rel1.empty())\n      as.insert(as.end(), ALL(rhs.assoc[y]));\n    else\n      for (long v: rel1[y])\n        as.insert(as.end(), ALL(rhs.assoc[v]));\n    sort_assoc(as);\n  };\n  if (! deterministic)\n    fsa = fsa.determinize(NULL, relate0);\n  if (! rhs.deterministic)\n    rhs.fsa = rhs.fsa.determinize(NULL, relate1);\n  fsa = fsa.intersect(rhs.fsa, relate);\n  assoc = move(new_assoc);\n  if (expr)\n    add_assoc(*expr);\n  deterministic = true;\n}\n\nvoid FsaAnno::minimize(vector<vector<long>>* mapping) {\n  assert(deterministic);\n  decltype(assoc) new_assoc;\n  auto relate = [&](vector<long>& xs) {\n    new_assoc.emplace_back();\n    auto& as = new_assoc.back();\n    for (long x: xs)\n      as.insert(as.end(), ALL(assoc[x]));\n    sort_assoc(as);\n    if (mapping)\n      mapping->push_back(xs);\n  };\n  fsa = fsa.distinguish(relate);\n  assoc = move(new_assoc);\n}\n\nvoid FsaAnno::union_(FsaAnno& rhs, UnionExpr* expr) {\n  long ln = fsa.n(), rn = rhs.fsa.n(), src = ln+rn,\n       old_lsrc = fsa.start;\n  fsa.start = src;\n  for (long f: rhs.fsa.finals)\n    fsa.finals.push_back(ln+f);\n  for (auto& es: rhs.fsa.adj) {\n    for (auto& e: es)\n      e.second += ln;\n    fsa.adj.emplace_back(move(es));\n  }\n  fsa.adj.emplace_back();\n  fsa.adj[src].emplace_back(epsilon, old_lsrc);\n  fsa.adj[src].emplace_back(epsilon, ln+rhs.fsa.start);\n  assoc.resize(fsa.n());\n  REP(i, rhs.fsa.n())\n    assoc[ln+i] = move(rhs.assoc[i]);\n  if (expr)\n    add_assoc(*expr);\n  deterministic = false;\n}\n\nvoid FsaAnno::plus(PlusExpr* expr) {\n  for (long f: fsa.finals)\n    emplace_front(fsa.adj[f], epsilon, fsa.start);\n  if (expr)\n    add_assoc(*expr);\n  deterministic = false;\n}\n\nvoid FsaAnno::question(QuestionExpr* expr) {\n  long src = fsa.n(), sink = src+1, old_src = fsa.start;\n  fsa.start = src;\n  fsa.adj.emplace_back();\n  fsa.adj.emplace_back();\n  fsa.adj[src].emplace_back(epsilon, old_src);\n  fsa.adj[src].emplace_back(epsilon, sink);\n  fsa.finals.push_back(sink);\n  assoc.resize(fsa.n());\n  if (expr)\n    add_assoc(*expr);\n  deterministic = false;\n}\n\nvoid FsaAnno::repeat(RepeatExpr& expr) {\n  FsaAnno r = epsilon_fsa(NULL);\n  REP(i, expr.low) {\n    FsaAnno t = *this;\n    r.concat(t, NULL);\n  }\n  if (expr.high == LONG_MAX) {\n    star(NULL);\n    r.concat(*this, NULL);\n  } else if (expr.low < expr.high) {\n    FsaAnno rhs = epsilon_fsa(NULL), x = *this;\n    ROF(i, 0, expr.high-expr.low) {\n      FsaAnno t = x;\n      rhs.union_(t, NULL);\n      if (i) {\n        t = *this;\n        x.concat(t, NULL);\n      }\n    }\n    r.concat(rhs, NULL);\n  }\n  r.deterministic = false;\n  *this = move(r);\n}\n\nvoid FsaAnno::star(StarExpr* expr) {\n  long src = fsa.n(), sink = src+1, old_src = fsa.start;\n  fsa.start = src;\n  fsa.adj.emplace_back();\n  fsa.adj.emplace_back();\n  fsa.adj[src].emplace_back(epsilon, old_src);\n  fsa.adj[src].emplace_back(epsilon, sink);\n  for (long f: fsa.finals) {\n    sorted_emplace(fsa.adj[f], epsilon, old_src);\n    sorted_emplace(fsa.adj[f], epsilon, sink);\n  }\n  fsa.finals.assign(1, sink);\n  assoc.resize(fsa.n());\n  if (expr)\n    add_assoc(*expr);\n  deterministic = false;\n}\n\nFsaAnno FsaAnno::bracket(BracketExpr& expr) {\n  FsaAnno r;\n  r.fsa.start = 0;\n  r.fsa.finals = {1};\n  r.fsa.adj.resize(2);\n  for (auto& x: expr.intervals.to)\n    r.fsa.adj[0].emplace_back(x, 1);\n  r.assoc.resize(2);\n  r.add_assoc(expr);\n  r.deterministic = true;\n  return r;\n}\n\nFsaAnno FsaAnno::call(CallExpr& expr) {\n  // represented by (0, special, 1)\n  FsaAnno r;\n  r.fsa.start = 0;\n  r.fsa.finals = {1};\n  r.fsa.adj.resize(2);\n  r.fsa.adj[0].emplace_back(make_pair(call_label, call_label+1), 1);\n  call_label++;\n  r.assoc.resize(2);\n  r.add_assoc(expr);\n  r.deterministic = true;\n  return r;\n}\n\nFsaAnno FsaAnno::collapse(CollapseExpr& expr) {\n  // represented by (0, special, 1)\n  FsaAnno r;\n  r.fsa.start = 0;\n  r.fsa.finals = {1};\n  r.fsa.adj.resize(2);\n  r.fsa.adj[0].emplace_back(make_pair(collapse_label, collapse_label+1), 1);\n  collapse_label++;\n  r.assoc.resize(2);\n  r.add_assoc(expr);\n  r.deterministic = true;\n  return r;\n}\n\nFsaAnno FsaAnno::dot(DotExpr* expr) {\n  FsaAnno r;\n  r.fsa.start = 0;\n  r.fsa.finals = {1};\n  r.fsa.adj.resize(2);\n  r.fsa.adj[0].emplace_back(make_pair(0L, AB), 1);\n  r.assoc.resize(2);\n  if (expr)\n    r.add_assoc(*expr);\n  r.deterministic = true;\n  return r;\n}\n\nFsaAnno FsaAnno::embed(EmbedExpr& expr) {\n  if (expr.define_stmt) {\n    FsaAnno r = compiled[expr.define_stmt];\n    // change the labels to differentiate instances of CallExpr\n    REP(i, r.fsa.n()) {\n      auto it = upper_bound(ALL(r.fsa.adj[i]), make_pair(make_pair(call_label_base, LONG_MAX), LONG_MAX));\n      if (it != r.fsa.adj[i].begin() && call_label_base < (it-1)->first.second)\n        --it;\n      for (; it != r.fsa.adj[i].end() && it->first.first < call_label; ++it) {\n        assert(call_label_base <= it->first.first);\n        long t = it->first.second-it->first.first;\n        it->first.first = call_label;\n        call_label += t;\n        it->first.second = call_label;\n        assert(it->first.second <= call_label);\n      }\n    }\n    r.add_assoc(expr);\n    return r;\n  } else { // macro\n    FsaAnno r;\n    r.fsa.start = 0;\n    r.fsa.finals = {1};\n    r.fsa.adj.resize(2);\n    r.fsa.adj[0].emplace_back(make_pair(expr.macro_value, expr.macro_value+1), 1);\n    r.assoc.resize(2);\n    r.add_assoc(expr);\n    r.deterministic = true;\n    return r;\n  }\n}\n\nFsaAnno FsaAnno::literal(LiteralExpr& expr) {\n  FsaAnno r;\n  r.fsa.start = 0;\n  long len = 0;\n  if (opt_bytes) {\n    len = expr.literal.size();\n    r.fsa.adj.resize(len+1);\n    REP(i, expr.literal.size()) {\n      long c = (u8)expr.literal[i];\n      r.fsa.adj[i].emplace_back(make_pair(c, c+1), i+1);\n    }\n  } else {\n    for (i32 c, i = 0; i < expr.literal.size(); len++) {\n      U8_NEXT_OR_FFFD(expr.literal.c_str(), i, expr.literal.size(), c);\n      r.fsa.adj.emplace_back();\n      r.fsa.adj[len].emplace_back(make_pair(c, c+1), len+1);\n    }\n    r.fsa.adj.emplace_back();\n  }\n  r.fsa.finals.push_back(len);\n  r.assoc.resize(len+1);\n  r.add_assoc(expr);\n  r.deterministic = true;\n  return r;\n}\n\nvoid FsaAnno::substring_grammar() {\n  long src = fsa.n(), sink = src+1, old_src = fsa.start;\n  fsa.start = src;\n  fsa.adj.emplace_back();\n  fsa.adj.emplace_back();\n  REP(i, src) {\n    bool ok = true;\n    for (auto aa: assoc[i])\n      if (auto e = dynamic_cast<CollapseExpr*>(aa.first)) {\n        if (e->define_stmt->intact && has_inner(aa.second)) {\n          ok = false;\n          break;\n        }\n      } else if (aa.first->stmt->intact && has_inner(aa.second)) {\n        ok = false;\n        break;\n      }\n    if (ok || i == old_src)\n      fsa.adj[src].emplace_back(epsilon, i);\n    if (ok || fsa.is_final(i))\n      emplace_front(fsa.adj[i], epsilon, sink);\n  }\n  fsa.finals.assign(1, sink);\n  assoc.resize(fsa.n());\n  deterministic = false;\n}\n"
  },
  {
    "path": "yanshi/src/fsa_anno.hh",
    "content": "#pragma once\n#include \"fsa.hh\"\n#include \"syntax.hh\"\n\nenum class ExprTag {\n  start = 1,\n  inner = 2,\n  final = 4,\n};\n\nextern inline bool has_start(ExprTag x) { return long(x) & long(ExprTag::start); }\nextern inline bool has_inner(ExprTag x) { return long(x) & long(ExprTag::inner); }\nextern inline bool has_final(ExprTag x) { return long(x) & long(ExprTag::final); }\n\nbool operator<(ExprTag x, ExprTag y);\nbool assoc_has_expr(vector<pair<Expr*, ExprTag>>& as, const Expr* x);\n\nstruct FsaAnno {\n  bool deterministic;\n  Fsa fsa;\n  vector<vector<pair<Expr*, ExprTag>>> assoc;\n  void accessible(const vector<long>* starts, vector<long>& mapping);\n  void add_assoc(Expr& expr);\n  void complement(ComplementExpr* expr);\n  void co_accessible(const vector<bool>* final, vector<long>& mapping);\n  void concat(FsaAnno& rhs, ConcatExpr* expr);\n  void determinize(const vector<long>* starts, vector<vector<long>>* mapping);\n  void difference(FsaAnno& rhs, DifferenceExpr* expr);\n  void intersect(FsaAnno& rhs, IntersectExpr* expr);\n  void minimize(vector<vector<long>>* mapping);\n  void plus(PlusExpr* expr);\n  void question(QuestionExpr* expr);\n  void repeat(RepeatExpr& expr);\n  void star(StarExpr* expr);\n  void substring_grammar();\n  void union_(FsaAnno& rhs, UnionExpr* expr);\n  static FsaAnno bracket(BracketExpr& expr);\n  static FsaAnno call(CallExpr& expr);\n  static FsaAnno collapse(CollapseExpr& expr);\n  static FsaAnno dot(DotExpr* expr);\n  static FsaAnno embed(EmbedExpr& expr);\n  static FsaAnno epsilon_fsa(EpsilonExpr* expr);\n  static FsaAnno literal(LiteralExpr& expr);\n};\n"
  },
  {
    "path": "yanshi/src/lexer.l",
    "content": "%{\n#include \"lexer_helper.hh\"\n#include \"option.hh\"\n#include \"parser.hh\"\n#include \"syntax.hh\"\n\n#include <limits.h>\n#include <stdlib.h>\n#include <string>\n#include <string.h>\n#include <unicode/utf8.h>\nusing namespace std;\n\n#define YY_USER_ACTION                      \\\n  do {                                      \\\n    yylloc->start = yyget_extra(yyscanner); \\\n    yylloc->end = yylloc->start + yyleng;   \\\n    yyset_extra(yylloc->end, yyscanner);    \\\n  } while (0);\n\nstatic string tmp_bracket, tmp_str;\nstatic long tmp_str_pos;\nstatic bool semicolon;\n\nstatic long invalid_escape(YYSTYPE* yylval, const char* text)\n{\n  yylval->errmsg = aprintf(\"invalid \\\\-escape: %s\", text);\n  return INVALID_CHARACTER;\n}\n\nstatic int invalid_escape_octonary(YYSTYPE* yylval, const char* text)\n{\n  yylval->errmsg = aprintf(\"invalid number after \\\\-escape: %s\", text);\n  return INVALID_CHARACTER;\n}\n\nstatic int invalid_escape_x(YYSTYPE* yylval, const char* text)\n{\n  yylval->errmsg = aprintf(\"invalid number after \\\\x-escape: %s\", text);\n  return INVALID_CHARACTER;\n}\n\nstatic int invalid_escape_u(YYSTYPE* yylval, const char* text)\n{\n  yylval->errmsg = aprintf(\"invalid number after \\\\u-escape: %s\", text);\n  return INVALID_CHARACTER;\n}\n\nstatic int invalid_escape_U(YYSTYPE* yylval, const char* text)\n{\n  yylval->errmsg = aprintf(\"invalid number after \\\\U-escape: %s\", text);\n  return INVALID_CHARACTER;\n}\n\nstatic void unexpected_eof(YYSTYPE* yylval, const char* token_end)\n{\n  yylval->errmsg = aprintf(\"missing %s at end of file\", token_end);\n}\n\nstatic void unexpected_newline(YYSTYPE* yylval, const char* token_end)\n{\n  yylval->errmsg = aprintf(\"missing %s at end of line\", token_end);\n}\n\nstatic int unexpected_codepoint(YYSTYPE* yylval)\n{\n  yylval->errmsg = aprintf(\"cannot use Unicode codepoints\");\n  return INVALID_CHARACTER;\n}\n\nextern \"C\" int raw_yywrap(yyscan_t yyscanner)\n{\n  semicolon = false;\n  return 1;\n}\n%}\n\n%option yywrap noinput\n%option reentrant\n%option extra-type=\"long\"\n%option bison-bridge bison-locations\n%option prefix=\"raw_yy\"\n%option stack\n\n%x EXPECT_CODE\n%x AFTER_ACTION_OP\n%x AFTER_EXPORT\n%x IN_BRACE\n%x IN_CODE\n%x IN_COMMENT\n%x IN_BRACKET\n%x IN_BRACKET_FIRST\n%x IN_LINE_COMMENT\n%s IN_PAREN\n%x IN_Q_STRING\n%x IN_QQ_STRING\n\nD\t\t\t[0-9]\nH\t\t\t[0-9A-Fa-f]\nL\t\t\t[a-zA-Z_\\x80-\\xff]\n\n%%\n\n\"::\" return COLONCOLON;\n\"..\" return DOTDOT;\n\"&&\" return AMPERAMPER;\n\";\" if (semicolon) return '\\n';\n[-~!&*=+,.?|{}:] return yytext[0];\n\"action\" yy_push_state(EXPECT_CODE, yyscanner); return ACTION;\n\"as\" return AS;\n\"c++\" yy_push_state(EXPECT_CODE, yyscanner); return CPP;\n\"epsilon\" return EPSILON;\n\"export\" yy_push_state(AFTER_EXPORT, yyscanner); return EXPORT;\n\"import\" return IMPORT;\n\"intact\" return INTACT;\n\"semicolon\" semicolon = true;\n\"nosemicolon\" semicolon = false;\n{L}({L}|{D})* yylval->str = new string(yytext); return IDENT;\n{D}+ yylval->integer = atol(yytext); return INTEGER;\n\"#define\" return PREPROCESS_DEFINE;\n\n\"#\" yy_push_state(IN_LINE_COMMENT, yyscanner);\n\"//\" yy_push_state(IN_LINE_COMMENT, yyscanner);\n<IN_LINE_COMMENT>{\n  \"\\n\" yy_pop_state(yyscanner); unput('\\n'); yyset_extra(yylloc->end-1, yyscanner);\n  <<EOF>> yy_pop_state(yyscanner);\n  . {}\n}\n\n\"/*\" yy_push_state(IN_COMMENT, yyscanner);\n<IN_COMMENT>{\n  \"*/\" yy_pop_state(yyscanner);\n  <<EOF>> yy_pop_state(yyscanner);\n  .|\\n {}\n}\n\n\"(\" yy_push_state(IN_PAREN, yyscanner); return '(';\n\")\" {\n  if (YY_START != IN_PAREN) {\n    unexpected_newline(yylval, \")\");\n    return INVALID_CHARACTER;\n  }\n  yy_pop_state(yyscanner);\n  return ')';\n}\n\n\"[\" yy_push_state(IN_BRACKET_FIRST, yyscanner); return '[';\n<IN_BRACKET_FIRST>{\n  \"^\" BEGIN IN_BRACKET; return '^';\n  [^-\\\\\\]\\n] { yy_pop_state(yyscanner); yy_push_state(IN_BRACKET, yyscanner); yylval->integer = yytext[0]; return CHAR; }\n  \"-\" { yylval->integer = '-'; return CHAR; }\n}\n<IN_BRACKET>{\n  \"]\" {\n    yy_pop_state(yyscanner);\n    if (YY_START == INITIAL || YY_START == IN_PAREN)\n      return ']';\n  }\n  [^-\\\\\\]\\n] yylval->integer = yytext[0]; return CHAR;\n}\n<IN_BRACKET_FIRST,IN_BRACKET>{\n  \\\\[0-7]+ {\n    BEGIN IN_BRACKET;\n    long c = strtol(yytext+1, NULL, 8);\n    if (UCHAR_MAX < c)\n      return invalid_escape_octonary(yylval, yytext);\n    yylval->integer = c;\n    return CHAR;\n  }\n  \\\\u[0-9a-fA-F]+ {\n    BEGIN IN_BRACKET;\n    if (opt_bytes)\n      return unexpected_codepoint(yylval);\n    long c = strtol(yytext+2, NULL, 16), len = 0;\n    if (UINT16_MAX < c)\n      return invalid_escape_u(yylval, yytext);\n    yylval->integer = c;\n    return CHAR;\n  }\n  \\\\U[0-9a-fA-F]+ {\n    BEGIN IN_BRACKET;\n    if (opt_bytes)\n      return unexpected_codepoint(yylval);\n    long c = strtol(yytext+2, NULL, 16), len = 0;\n    if (MAX_CODEPOINT < c)\n      return invalid_escape_U(yylval, yytext);\n    yylval->integer = c;\n    return CHAR;\n  }\n  \\\\x[0-9a-fA-F]+ {\n    BEGIN IN_BRACKET;\n    long c = strtol(yytext+2, NULL, 16);\n    if (UCHAR_MAX < c)\n      return invalid_escape_x(yylval, yytext);\n    yylval->integer = c;\n    return CHAR;\n  }\n  \\\\a BEGIN IN_BRACKET; yylval->integer = '\\a'; return CHAR;\n  \\\\b BEGIN IN_BRACKET; yylval->integer = '\\b'; return CHAR;\n  \\\\f BEGIN IN_BRACKET; yylval->integer = '\\f'; return CHAR;\n  \\\\n BEGIN IN_BRACKET; yylval->integer = '\\n'; return CHAR;\n  \\\\r BEGIN IN_BRACKET; yylval->integer = '\\r'; return CHAR;\n  \\\\t BEGIN IN_BRACKET; yylval->integer = '\\t'; return CHAR;\n  \\\\v BEGIN IN_BRACKET; yylval->integer = '\\v'; return CHAR;\n  \\\\. BEGIN IN_BRACKET; yylval->integer = yytext[1]; return CHAR;\n  - BEGIN IN_BRACKET; return '-';\n  \"\\n\" unexpected_newline(yylval, \"]\"); return INVALID_CHARACTER;\n  <<EOF>> yy_pop_state(yyscanner); unexpected_eof(yylval, \"]\");\n}\n\n<AFTER_EXPORT>{ // optional 'BRACED_CODE' to specify extra parameters\n  \"intact\" yy_pop_state(yyscanner); return INTACT;\n  {L}({L}|{D})* yy_pop_state(yyscanner); yylval->str = new string(yytext); return IDENT;\n  \"{\" BEGIN IN_CODE; tmp_bracket.clear();\n  [ \\t\\n] {}\n  <<EOF>> yy_pop_state(yyscanner);\n}\n\n<EXPECT_CODE>{\n  {L}({L}|{D})* yylval->str = new string(yytext); return IDENT;\n  \"{\" BEGIN IN_CODE; tmp_bracket.clear();\n  [ \\t\\n] {}\n  <<EOF>> yy_pop_state(yyscanner);\n}\n\n[>@%$] yy_push_state(AFTER_ACTION_OP, yyscanner); return yytext[0];\n<AFTER_ACTION_OP>{\n  -?{D}+ yylval->integer = atol(yytext); return INTEGER;\n  {L}({L}|{D})* yy_pop_state(yyscanner); yylval->str = new string(yytext); return IDENT;\n  \"{\" BEGIN IN_CODE; tmp_bracket.clear();\n  [ \\t\\n]+ {}\n  <<EOF>> yy_pop_state(yyscanner);\n  . yylval->errmsg = strdup(\"invalid character\"); return INVALID_CHARACTER;\n}\n<IN_CODE>{\n  \"'\" { tmp_bracket += '\\''; yy_push_state(IN_Q_STRING, yyscanner); }\n  \"\\\"\" { tmp_bracket += '\"'; yy_push_state(IN_QQ_STRING, yyscanner); }\n  \"{\" { tmp_bracket += '{'; yy_push_state(IN_CODE, yyscanner); }\n  \"}\" {\n    yy_pop_state(yyscanner);\n    if (YY_START == INITIAL || YY_START == IN_PAREN) {\n      yylval->str = new string(tmp_bracket);\n      return BRACED_CODE;\n    } else\n      tmp_bracket += '}';\n  }\n  .|\"\\n\" tmp_bracket += yytext[0];\n  <<EOF>> yy_pop_state(yyscanner); unexpected_eof(yylval, \"}\");\n}\n\n' tmp_str.clear(); tmp_str_pos = yylloc->start; yy_push_state(IN_Q_STRING, yyscanner);\n\"\\\"\" tmp_str.clear(); tmp_str_pos = yylloc->start; yy_push_state(IN_QQ_STRING, yyscanner);\n<IN_Q_STRING>{\n  ' {\n    yy_pop_state(yyscanner);\n    if (YY_START == INITIAL || YY_START == IN_PAREN) {\n      yylval->str = new string(tmp_str);\n      yylloc->start = tmp_str_pos;\n      return STRING_LITERAL;\n    }\n    tmp_bracket += yytext;\n  }\n  <<EOF>> yy_pop_state(yyscanner); unexpected_eof(yylval, \"'\");\n}\n<IN_QQ_STRING>{\n  \"\\\"\" {\n    yy_pop_state(yyscanner);\n    if (YY_START == INITIAL || YY_START == IN_PAREN) {\n      yylval->str = new string(tmp_str);\n      yylloc->start = tmp_str_pos;\n      return STRING_LITERAL;\n    }\n    tmp_bracket += yytext;\n  }\n  <<EOF>> yy_pop_state(yyscanner); unexpected_eof(yylval, \"\\\"\");\n}\n\n<IN_Q_STRING,IN_QQ_STRING>{\n  \\\\[0-7]+ {\n    long c = strtol(yytext+1, NULL, 8);\n    if (UCHAR_MAX < c)\n      return invalid_escape_octonary(yylval, yytext);\n    tmp_str.push_back(c);\n    tmp_bracket += yytext;\n  }\n  \\\\x[0-9a-fA-F]+ {\n    long c = strtol(yytext+2, NULL, 16);\n    if (UCHAR_MAX < c)\n      return invalid_escape_x(yylval, yytext);\n    tmp_str.push_back(c);\n    tmp_bracket += yytext;\n  }\n  \\\\u[0-9a-fA-F]+ {\n    char s[4];\n    long c = strtol(yytext+2, NULL, 16), len = 0;\n    if (UINT16_MAX < c)\n      return invalid_escape_u(yylval, yytext);\n    U8_APPEND_UNSAFE(s, len, c);\n    tmp_str.insert(tmp_str.end(), s, s+len);\n    tmp_bracket += yytext;\n  }\n  \\\\U[0-9a-fA-F]+ {\n    char s[4];\n    long c = strtol(yytext+2, NULL, 16), len = 0;\n    if (MAX_CODEPOINT < c)\n      return invalid_escape_U(yylval, yytext);\n    U8_APPEND_UNSAFE(s, len, c);\n    tmp_str.insert(tmp_str.end(), s, s+len);\n    tmp_bracket += yytext;\n  }\n  \\\\a tmp_str += '\\a'; tmp_bracket += yytext;\n  \\\\b tmp_str += '\\b'; tmp_bracket += yytext;\n  \\\\f tmp_str += '\\f'; tmp_bracket += yytext;\n  \\\\n tmp_str += '\\n'; tmp_bracket += yytext;\n  \\\\r tmp_str += '\\r'; tmp_bracket += yytext;\n  \\\\t tmp_str += '\\t'; tmp_bracket += yytext;\n  \\\\v tmp_str += '\\v'; tmp_bracket += yytext;\n  \\\\[\\n\\\"\\'?\\\\] tmp_str += yytext[1]; tmp_bracket += yytext;\n  \\\\. return invalid_escape(yylval, yytext);\n  .|\\n tmp_str += yytext[0]; tmp_bracket += yytext[0];\n}\n\n\\\\\\n {}\n\"\\n\" if (YY_START == INITIAL && ! semicolon) return '\\n';\n[ \\t]+ {}\n. { yylval->errmsg = strdup(\"invalid character\"); return INVALID_CHARACTER; }\n"
  },
  {
    "path": "yanshi/src/lexer_helper.cc",
    "content": "#include \"common.hh\"\n\n#include <cstdarg>\n#include <cstdio>\nusing namespace std;\n\nchar* aprintf(const char* fmt, ...)\n{\n  va_list va;\n  va_start(va, fmt);\n  char* r = NULL;\n  vasprintf(&r, fmt, va);\n  va_end(va);\n  return r;\n}\n"
  },
  {
    "path": "yanshi/src/lexer_helper.hh",
    "content": "#pragma once\n\nchar* aprintf(const char* fmt, ...)\n  __attribute__((format(printf, 1, 2)));\n"
  },
  {
    "path": "yanshi/src/loader.cc",
    "content": "#include \"common.hh\"\n#include \"compiler.hh\"\n#include \"loader.hh\"\n#include \"option.hh\"\n#include \"parser.hh\"\n#include \"repl.hh\"\n\n#include <algorithm>\n#include <errno.h>\n#include <functional>\n#include <stdio.h>\n#include <stack>\n#include <string.h>\n#include <sys/stat.h>\n#include <sysexits.h>\n#include <unordered_map>\n#include <unordered_set>\n#include <utility>\nusing namespace std;\n\nstatic map<pair<dev_t, ino_t>, Module> inode2module;\nstatic unordered_map<DefineStmt*, vector<DefineStmt*>> depended_by; // key ranges over all DefineStmt\nmap<DefineStmt*, vector<Expr*>> used_as_call, used_as_collapse, used_as_embed;\nstatic DefineStmt* main_export;\nModule* main_module;\nFILE *output, *output_header;\n\nvoid print_module_info(Module& mo)\n{\n  yellow(); printf(\"filename: %s\\n\", mo.filename.c_str());\n  cyan(); puts(\"qualified imports:\"); sgr0();\n  for (auto& x: mo.qualified_import)\n    printf(\"  %s as %s\\n\", x.second->filename.c_str(), x.first.c_str());\n  cyan(); puts(\"unqualified imports:\"); sgr0();\n  for (auto& x: mo.unqualified_import)\n    printf(\"  %s\\n\", x->filename.c_str());\n  cyan(); puts(\"defined actions:\"); sgr0();\n  for (auto& x: mo.defined_action)\n    printf(\"  %s\\n\", x.first.c_str());\n  cyan(); puts(\"defined:\"); sgr0();\n  for (auto& x: mo.defined)\n    printf(\"  %s\\n\", x.first.c_str());\n}\n\nStmt* resolve(Module& mo, const string qualified, const string& ident)\n{\n  if (qualified.size()) {\n    if (! mo.qualified_import.count(qualified))\n      return NULL;\n    auto it = mo.qualified_import[qualified]->defined.find(ident);\n    if (it == mo.qualified_import[qualified]->defined.end())\n      return NULL;\n    return it->second;\n  } else {\n    Stmt* r = NULL;\n    if (mo.macro.count(ident))\n      r = mo.macro[ident];\n    if (mo.defined.count(ident)) {\n      if (r) return (Stmt*)1;\n      r = mo.defined[ident];\n    }\n    for (auto* import: mo.unqualified_import) {\n      if (import->macro.count(ident)) {\n        if (r) return (Stmt*)1;\n        r = import->macro[ident];\n      }\n      if (import->defined.count(ident)) {\n        if (r) return (Stmt*)1;\n        r = import->defined[ident];\n      }\n    }\n    return r;\n  }\n}\n\nActionStmt* resolve_action(Module& mo, const string qualified, const string& ident)\n{\n  if (qualified.size()) {\n    if (! mo.qualified_import.count(qualified))\n      return NULL;\n    auto it = mo.qualified_import[qualified]->defined_action.find(ident);\n    if (it == mo.qualified_import[qualified]->defined_action.end())\n      return NULL;\n    return it->second;\n  } else {\n    ActionStmt* r = NULL;\n    if (mo.defined_action.count(ident))\n      r = mo.defined_action[ident];\n    for (auto* import: mo.unqualified_import)\n      if (import->defined_action.count(ident)) {\n        if (r) return (ActionStmt*)1;\n        r = import->defined_action[ident];\n      }\n    return r;\n  }\n}\n\nstruct ModuleImportDef : PreorderStmtVisitor {\n  Module& mo;\n  long& n_errors;\n  ModuleImportDef(Module& mo, long& n_errors) : mo(mo), n_errors(n_errors) {}\n\n  void visit(ActionStmt& stmt) override {\n    if (mo.defined_action.count(stmt.ident)) {\n      n_errors++;\n      mo.locfile.error(stmt.loc, \"redefined '%s'\", stmt.ident.c_str());\n    } else\n      mo.defined_action[stmt.ident] = &stmt;\n  }\n  // TODO report error: import 'aa.hs' (#define d 3) ; #define d 4\n  void visit(DefineStmt& stmt) override {\n    if (mo.defined.count(stmt.lhs) || mo.macro.count(stmt.lhs)) {\n      n_errors++;\n      mo.locfile.error(stmt.loc, \"redefined '%s'\", stmt.lhs.c_str());\n    } else {\n      mo.defined.emplace(stmt.lhs, &stmt);\n      stmt.module = &mo;\n      depended_by[&stmt]; // empty\n    }\n  }\n  void visit(ImportStmt& stmt) override {\n    Module* m = load_module(n_errors, stmt.filename);\n    if (! m) {\n      n_errors++;\n      mo.locfile.error(stmt.loc, \"'%s': %s\", stmt.filename.c_str(), errno ? strerror(errno) : \"parse error\");\n      return;\n    }\n    if (stmt.qualified.size())\n      mo.qualified_import[stmt.qualified] = m;\n    else if (count(ALL(mo.unqualified_import), m) == 0)\n      mo.unqualified_import.push_back(m);\n  }\n  void visit(PreprocessDefineStmt& stmt) override {\n    if (mo.defined.count(stmt.ident) || mo.macro.count(stmt.ident)) {\n      n_errors++;\n      mo.locfile.error(stmt.loc, \"redefined '%s'\", stmt.ident.c_str());\n    } else\n      mo.macro[stmt.ident] = &stmt;\n  }\n};\n\nstruct ModuleUse : PrePostActionExprStmtVisitor {\n  Module& mo;\n  long& n_errors;\n  DefineStmt* stmt = NULL;\n  ModuleUse(Module& mo, long& n_errors) : mo(mo), n_errors(n_errors) {}\n\n  void pre_expr(Expr& expr) override {\n    expr.stmt = stmt;\n  }\n\n  void post_expr(Expr& expr) override {\n    for (auto a: expr.entering)\n      PrePostActionExprStmtVisitor::visit(*a.first);\n    for (auto a: expr.finishing)\n      PrePostActionExprStmtVisitor::visit(*a.first);\n    for (auto a: expr.leaving)\n      PrePostActionExprStmtVisitor::visit(*a.first);\n    for (auto a: expr.transiting)\n      PrePostActionExprStmtVisitor::visit(*a.first);\n  }\n\n  void visit(RefAction& action) override {\n    ActionStmt* r = resolve_action(mo, action.qualified, action.ident);\n    if (! r) {\n      n_errors++;\n      if (action.qualified.size())\n        mo.locfile.error(action.loc, \"'%s::%s' undefined\", action.qualified.c_str(), action.ident.c_str());\n      else\n        mo.locfile.error(action.loc, \"'%s' undefined\", action.ident.c_str());\n    } else if (r == (Stmt*)1) {\n      n_errors++;\n      mo.locfile.error(action.loc, \"'%s' redefined\", action.ident.c_str());\n    } else\n      action.define_stmt = r;\n  }\n\n  void visit(BracketExpr& expr) override {\n    for (auto& x: expr.intervals.to)\n      AB = max(AB, x.second);\n  }\n  void visit(CallExpr& expr) override {\n    Stmt* r = resolve(mo, expr.qualified, expr.ident);\n    if (! r)\n      error_undefined(expr.loc, expr.qualified, expr.ident);\n    else if (r == (Stmt*)1)\n      error_ambiguous(expr.loc, expr.ident);\n    else if (auto d = dynamic_cast<PreprocessDefineStmt*>(r))\n      error_misuse_macro(\"CallExpr\", expr.loc, expr.qualified, expr.ident);\n    else if (auto d = dynamic_cast<DefineStmt*>(r)) {\n      used_as_call[d].push_back(&expr);\n      expr.define_stmt = d;\n    } else\n      assert(0);\n  }\n  void visit(CollapseExpr& expr) override {\n    Stmt* r = resolve(mo, expr.qualified, expr.ident);\n    if (! r)\n      error_undefined(expr.loc, expr.qualified, expr.ident);\n    else if (r == (Stmt*)1)\n      error_ambiguous(expr.loc, expr.ident);\n    else if (auto d = dynamic_cast<PreprocessDefineStmt*>(r))\n      error_misuse_macro(\"CollapseExpr\", expr.loc, expr.qualified, expr.ident);\n    else if (auto d = dynamic_cast<DefineStmt*>(r)) {\n      used_as_collapse[d].push_back(&expr);\n      expr.define_stmt = d;\n    } else\n      assert(0);\n  }\n  void visit(DefineStmt& stmt) override {\n    this->stmt = &stmt;\n    PrePostActionExprStmtVisitor::visit(*stmt.rhs);\n    this->stmt = NULL;\n  }\n  void visit(EmbedExpr& expr) override {\n    // introduce dependency\n    Stmt* r = resolve(mo, expr.qualified, expr.ident);\n    if (! r)\n      error_undefined(expr.loc, expr.qualified, expr.ident);\n    else if (r == (Stmt*)1)\n      error_ambiguous(expr.loc, expr.ident);\n    else if (auto d = dynamic_cast<PreprocessDefineStmt*>(r)) {\n      // enlarge alphabet\n      expr.define_stmt = NULL;\n      expr.macro_value = d->value;\n      AB = max(AB, d->value+1);\n    } else if (auto d = dynamic_cast<DefineStmt*>(r)) {\n      depended_by[d].push_back(stmt);\n      used_as_embed[d].push_back(&expr);\n      expr.define_stmt = d;\n    } else\n      assert(0);\n  }\nprivate:\n  void error_undefined(const Location& loc, const string& qualified, const string& ident) {\n    n_errors++;\n    if (qualified.size())\n      mo.locfile.error(loc, \"'%s::%s' undefined\", qualified.c_str(), ident.c_str());\n    else\n      mo.locfile.error(loc, \"'%s' undefined\", ident.c_str());\n  }\n  void error_ambiguous(const Location& loc, const string& ident) {\n    n_errors++;\n    mo.locfile.error(loc, \"ambiguous '%s'\", ident.c_str());\n  }\n  void error_misuse_macro(const char* name, const Location& loc, const string& qualified, const string& ident) {\n    n_errors++;\n    if (qualified.size())\n      mo.locfile.error(loc, \"macro '%s::%s' used as %s\", qualified.c_str(), ident.c_str(), name);\n    else\n      mo.locfile.error(loc, \"macro '%s' used as %s\", ident.c_str(), name);\n  }\n};\n\nModule* load_module(long& n_errors, const string& filename)\n{\n  FILE* file = stdin;\n  if (filename != \"-\") {\n    file = fopen(filename.c_str(), \"r\");\n    for (string& include: opt_include_paths) {\n      if (file) break;\n      file = fopen((include+'/'+filename).c_str(), \"r\");\n    }\n  }\n  if (! file) {\n    n_errors++;\n    return NULL;\n  }\n\n  pair<dev_t, ino_t> inode{0, 0}; // stdin -> {0, 0}\n  if (file != stdin) {\n    struct stat sb;\n    if (fstat(fileno(file), &sb) < 0)\n      err_exit(EX_OSFILE, \"fstat '%s'\", filename.c_str());\n    inode = {sb.st_dev, sb.st_ino};\n  }\n  if (inode2module.count(inode)) {\n    fclose(file);\n    return &inode2module[inode];\n  }\n  Module& mo = inode2module[inode];\n\n  string module{file != stdin ? filename : \"main\"};\n  string::size_type t = module.find('.');\n  if (t != string::npos)\n    module.erase(t, module.size()-t);\n\n  long r;\n  char buf[BUF_SIZE];\n  string data;\n  while ((r = fread(buf, 1, sizeof buf, file)) > 0) {\n    data += string(buf, buf+r);\n    if (r < sizeof buf) break;\n  }\n  fclose(file);\n  if (data.empty() || data.back() != '\\n')\n    data.push_back('\\n');\n  LocationFile locfile(filename, data);\n\n  Stmt* toplevel = NULL;\n  mo.locfile = locfile;\n  mo.filename = filename;\n  long errors = parse(locfile, toplevel);\n  if (! toplevel) {\n    n_errors += errors;\n    mo.status = BAD;\n    mo.toplevel = NULL;\n    return &mo;\n  }\n  mo.toplevel = toplevel;\n  return &mo;\n}\n\nstatic vector<DefineStmt*> topo_define_stmts(long& n_errors)\n{\n  vector<DefineStmt*> topo;\n  vector<DefineStmt*> st;\n  unordered_map<DefineStmt*, i8> vis; // 0: unvisited; 1: in stack; 2: visited; 3: in a cycle\n  unordered_map<DefineStmt*, long> cnt;\n  function<bool(DefineStmt*)> dfs = [&](DefineStmt* u) {\n    if (vis[u] == 2)\n      return false;\n    if (vis[u] == 3)\n      return true;\n    if (vis[u] == 1) {\n      u->module->locfile.error_context(u->loc, \"'%s': circular embedding\", u->lhs.c_str());\n      long i = st.size();\n      while (st[i-1] != u)\n        i--;\n      st.push_back(st[i-1]);\n      for (; i < st.size(); i++) {\n        vis[st[i]] = 3;\n        fputs(\"  \", stderr);\n        st[i]->module->locfile.error_context(st[i]->loc, \"required by %s\", st[i]->lhs.c_str());\n      }\n      fputs(\"\\n\", stderr);\n      return true;\n    }\n    cnt[u] = u->export_ ? 1 : 0;\n    vis[u] = 1;\n    st.push_back(u);\n    bool cycle = false;\n    for (auto v: depended_by[u])\n      if (dfs(v))\n        cycle = true;\n      else\n        cnt[u] += cnt[v];\n    st.pop_back();\n    vis[u] = 2;\n    topo.push_back(u);\n    return cycle;\n  };\n  for (auto& d: depended_by)\n    if (! vis[d.first] && dfs(d.first)) // detected cycle\n      n_errors++;\n  reverse(ALL(topo));\n  if (opt_dump_embed) {\n    magenta(); printf(\"=== Embed\\n\"); sgr0();\n    for (auto stmt: topo)\n      if (cnt[stmt] > 0)\n        printf(\"count(%s::%s) = %ld\\n\", stmt->module->filename.c_str(), stmt->lhs.c_str(), cnt[stmt]);\n  }\n  return topo;\n}\n\nlong load(const string& filename)\n{\n  long n_errors = 0;\n  Module* mo = load_module(n_errors, filename);\n  if (! mo) {\n    err_exit(EX_OSFILE, \"fopen\", filename.c_str());\n    return n_errors;\n  }\n  if (mo->status == BAD)\n    return n_errors;\n  main_module = mo;\n\n  DP(1, \"Processing import & def\");\n  for(;;) {\n    bool done = true;\n    for (auto& it: inode2module)\n      if (it.second.status == UNPROCESSED) {\n        done = false;\n        Module& mo = it.second;\n        mo.status = GOOD;\n        long old = n_errors;\n        ModuleImportDef p{mo, n_errors};\n        for (Stmt* x = mo.toplevel; x; x = x->next)\n          x->accept(p);\n        mo.status = old == n_errors ? GOOD : BAD;\n      }\n    if (done) break;\n  }\n  if (n_errors)\n    return n_errors;\n\n  DP(1, \"Processing use\");\n  for (auto& it: inode2module)\n    if (it.second.status == GOOD) {\n      Module& mo = it.second;\n      ModuleUse p{mo, n_errors};\n      for (Stmt* x = mo.toplevel; x; x = x->next)\n        x->accept(p);\n    }\n  if (n_errors)\n    return n_errors;\n\n  // warning: not used solely as CallExpr, CollapseExpr or EmbedExpr\n  {\n    auto it0 = used_as_call.begin(), it0e = used_as_call.end(),\n         it1 = used_as_collapse.begin(), it1e = used_as_collapse.end(),\n         it2 = used_as_embed.begin(), it2e = used_as_embed.end();\n    while (it0 != it0e || it1 != it1e || it2 != it2e) {\n      long k = 0;\n      long c = 0;\n      DefineStmt* x = NULL;\n      if (it0 != it0e && (! x || it0->first < x)) x = it0->first;\n      if (it1 != it1e && (! x || it1->first < x)) x = it1->first;\n      if (it2 != it2e && (! x || it2->first < x)) x = it2->first;\n      if (it0 != it0e && it0->first == x) c++;\n      if (it1 != it1e && it1->first == x) c++;\n      if (it2 != it2e && it2->first == x) c++;\n      if (c > 1) {\n        x->module->locfile.warning(x->loc, \"'%s' is not used solely as CallExpr, CollapseExpr or EmbedExpr\", x->lhs.c_str());\n        if (it0 != it0e && it0->first == x)\n          for (auto* y: it0->second) {\n            fputs(\"  \", stderr);\n            y->stmt->module->locfile.warning_context(y->loc, \"required by %s\", y->stmt->lhs.c_str());\n          }\n        if (it1 != it1e && it1->first == x)\n          for (auto* y: it1->second) {\n            fputs(\"  \", stderr);\n            y->stmt->module->locfile.warning_context(y->loc, \"required by %s\", y->stmt->lhs.c_str());\n          }\n        if (it2 != it2e && it2->first == x)\n          for (auto* y: it2->second) {\n            fputs(\"  \", stderr);\n            y->stmt->module->locfile.warning_context(y->loc, \"required by %s\", y->stmt->lhs.c_str());\n          }\n      }\n      if (it0 != it0e && it0->first == x) ++it0, c++;\n      if (it1 != it1e && it1->first == x) ++it1, c++;\n      if (it2 != it2e && it2->first == x) ++it2, c++;\n    }\n  }\n\n  if (opt_dump_module) {\n    magenta(); printf(\"=== Module\\n\"); sgr0();\n    for (auto& it: inode2module)\n      if (it.second.status == GOOD) {\n        Module& mo = it.second;\n        print_module_info(mo);\n      }\n    puts(\"\");\n  }\n\n  if (opt_dump_tree) {\n    magenta(); printf(\"=== Tree\\n\"); sgr0();\n    StmtPrinter p;\n    for (auto& it: inode2module)\n      if (it.second.status == GOOD) {\n        Module& mo = it.second;\n        yellow(); printf(\"filename: %s\\n\", mo.filename.c_str()); sgr0();\n        for (Stmt* x = mo.toplevel; x; x = x->next)\n          x->accept(p);\n      }\n    puts(\"\");\n  }\n\n  DP(1, \"Topological sorting\");\n  vector<DefineStmt*> topo = topo_define_stmts(n_errors);\n  if (n_errors)\n    return n_errors;\n\n  if (opt_check)\n    return 0;\n\n  // AB has been updated by ModuleUse\n  action_label_base = action_label = AB;\n  call_label_base = call_label = action_label+1000000;\n  collapse_label_base = collapse_label = call_label+1000000;\n\n  DP(1, \"Compiling DefineStmt\");\n  for (auto stmt: topo)\n    compile(stmt);\n\n  output = strcmp(opt_output_filename, \"-\") ? fopen(opt_output_filename, \"w\") : stdout;\n  if (! output) {\n    n_errors++;\n    err_exit(EX_OSFILE, \"fopen\", opt_output_filename);\n    return n_errors;\n  }\n\n  unordered_map<DefineStmt*, vector<pair<long, long>>> stmt2call_addr;\n  DP(1, \"Compiling exporting DefineStmt (coalescing referenced CallExpr/CollapseExpr)\");\n  for (Stmt* x = main_module->toplevel; x; x = x->next)\n    if (auto xx = dynamic_cast<DefineStmt*>(x))\n      if (xx->export_ && ! compile_export(xx))\n        n_errors++;\n  if (n_errors)\n    return n_errors;\n\n  for (Stmt* x = main_module->toplevel; x; x = x->next)\n    if (auto xx = dynamic_cast<DefineStmt*>(x))\n      if (xx->export_) {\n        FsaAnno& anno = compiled[xx];\n        if (opt_dump_automaton)\n          print_automaton(anno.fsa);\n        if (opt_dump_assoc)\n          print_assoc(anno);\n      }\n\n  if (opt_mode == Mode::cxx) {\n    if (opt_output_header_filename) {\n      output_header = fopen(opt_output_header_filename, \"w\");\n      if (! output_header) {\n        n_errors++;\n        err_exit(EX_OSFILE, \"fopen\", opt_output_header_filename);\n        return n_errors;\n      }\n    }\n    DP(1, \"Generating C++\");\n    generate_cxx(mo);\n    if (output_header)\n      fclose(output_header);\n  } else if (opt_mode == Mode::graphviz) {\n    DP(1, \"Generating Graphviz dot\");\n    generate_graphviz(mo);\n  } else if (opt_mode == Mode::interactive) {\n    DP(1, \"Testing given string\");\n    DefineStmt* main_export = NULL;\n    for (Stmt* x = main_module->toplevel; x; x = x->next)\n      if (auto xx = dynamic_cast<DefineStmt*>(x))\n        if (xx->export_) {\n          main_export = xx;\n          break;\n        }\n    if (! main_export)\n      puts(\"no exporting DefineStmt\");\n    else {\n      printf(\"Testing %s\\n\", main_export->lhs.c_str());\n      repl(main_export);\n    }\n  }\n\n  fclose(output);\n  return n_errors;\n}\n\nvoid unload_all()\n{\n  for (auto& it: inode2module) {\n    Module& mo = it.second;\n    stmt_free(mo.toplevel);\n  }\n}\n"
  },
  {
    "path": "yanshi/src/loader.hh",
    "content": "#pragma once\n#include \"syntax.hh\"\n\n#include <map>\n#include <set>\n#include <string>\n#include <unordered_map>\n#include <vector>\nusing std::map;\nusing std::set;\nusing std::string;\nusing std::unordered_map;\nusing std::vector;\n\nenum ModuleStatus { UNPROCESSED = 0, BAD, GOOD };\n\nstruct Module {\n  ModuleStatus status;\n  LocationFile locfile;\n  string filename;\n  Stmt* toplevel;\n  unordered_map<string, DefineStmt*> defined;\n  vector<Module*> unqualified_import;\n  unordered_map<string, Module*> qualified_import;\n  unordered_map<string, ActionStmt*> defined_action;\n  unordered_map<string, PreprocessDefineStmt*> macro;\n};\n\nStmt* resolve(Module& mo, const string qualified, const string& ident);\nlong load(const string& filename);\nModule* load_module(long& n_errors, const string& filename);\nvoid unload_all();\nextern Module* main_module;\nextern FILE *output, *output_header;\nextern map<DefineStmt*, vector<Expr*>> used_as_call, used_as_collapse, used_as_embed;\n"
  },
  {
    "path": "yanshi/src/location.cc",
    "content": "#include \"common.hh\"\n#include \"location.hh\"\n\n#include <algorithm>\n#include <cstdarg>\nusing namespace std;\n\nLocationFile::LocationFile(const string& filename, const string& data) : filename(filename), data(data)\n{\n  // data ends with '\\n'\n  long nlines = count(data.begin(), data.end(), '\\n');\n  linemap.assign(nlines+1, 0);\n  long line = 1;\n  for (long i = 0; i < data.size(); i++)\n    if (data[i] == '\\n')\n      linemap[line++] = i+1;\n}\n\nvoid LocationFile::context(const Location& loc) const\n{\n  long line1, col1, line2, col2;\n  locate(loc, line1, col1, line2, col2);\n  if (line1 == line2) {\n    fputs(\"  \", stderr);\n    FOR(i, linemap[line1], line1+1 < linemap.size() ? linemap[line1+1] : data.size()) {\n      if (i == loc.start)\n        magenta();\n      fputc(data[i], stderr);\n      if (i+1 == loc.end)\n        sgr0();\n    }\n  } else {\n    bool first = true;\n    fputs(\"  \", stderr);\n    FOR(i, linemap[line1], linemap[line1+1]) {\n      if (i == loc.start)\n        magenta();\n      fputc(data[i], stderr);\n    }\n    if (line2-line1 < 8) {\n      FOR(i, linemap[line1+1], linemap[line2]) {\n        if (first) { first = false; fputs(\"  \", stderr); }\n        fputc(data[i], stderr);\n        if (data[i] == '\\n') first = true;\n      }\n    } else {\n      FOR(i, linemap[line1+1], linemap[line1+4]) {\n        if (first) { first = false; fputs(\"  \", stderr); }\n        fputc(data[i], stderr);\n        if (data[i] == '\\n') first = true;\n      }\n      fputs(\"  ........\\n\", stderr);\n      FOR(i, linemap[line2-3], linemap[line2]) {\n        if (first) { first = false; fputs(\"  \", stderr); }\n        fputc(data[i], stderr);\n        if (data[i] == '\\n') first = true;\n      }\n    }\n    FOR(i, linemap[line2], line2+1 < linemap.size() ? linemap[line2+1] : data.size()) {\n      if (first) { first = false; fputs(\"  \", stderr); }\n      fputc(data[i], stderr);\n      if (i+1 == loc.end)\n        sgr0();\n    }\n  }\n}\n\nvoid LocationFile::locate(const Location& loc, long& line1, long& col1, long& line2, long& col2) const\n{\n  line1 = upper_bound(ALL(linemap), loc.start) - linemap.begin() - 1;\n  line2 = upper_bound(ALL(linemap), max(loc.end-1, 0L)) - linemap.begin() - 1;\n  col1 = loc.start - linemap[line1];\n  col2 = loc.end - linemap[line2];\n}\n\nvoid LocationFile::report_location(const Location& loc) const\n{\n  long line1, col1, line2, col2;\n  locate(loc, line1, col1, line2, col2);\n  yellow(2);\n  fprintf(stderr, \"%s \", filename.c_str());\n  cyan(2);\n  if (line1 == line2)\n    fprintf(stderr, \"%ld:%ld-%ld \", line1+1, col1+1, col2);\n  else\n    fprintf(stderr, \"%ld-%ld:%ld-%ld \", line1+1, line2+1, col1+1, col2);\n}\n\nvoid LocationFile::error(const Location& loc, const char* fmt, ...) const\n{\n  report_location(loc);\n  red(2);\n  fprintf(stderr, \"error \");\n  va_list va;\n  va_start(va, fmt);\n  vfprintf(stderr, fmt, va);\n  va_end(va);\n  fputs(\"\\n\", stderr);\n  sgr0(2);\n}\n\nvoid LocationFile::warning(const Location& loc, const char* fmt, ...) const\n{\n  report_location(loc);\n  yellow(2);\n  fprintf(stderr, \"warning \");\n  va_list va;\n  va_start(va, fmt);\n  vfprintf(stderr, fmt, va);\n  va_end(va);\n  fputs(\"\\n\", stderr);\n  sgr0(2);\n}\n"
  },
  {
    "path": "yanshi/src/location.hh",
    "content": "#pragma once\n#include \"common.hh\"\n\n#include <string>\n#include <utility>\n#include <vector>\n\nstruct Location { long start, end; };\n\nstruct LocationFile {\n  std::string filename, data;\n  std::vector<long> linemap;\n\n  LocationFile() = default;\n  LocationFile(const std::string& filename, const std::string& data);\n  LocationFile& operator=(const LocationFile&) = default;\n  void context(const Location& loc) const;\n  void locate(const Location& loc, long& line1, long& col1, long& line2, long& col2) const;\n  void report_location(const Location& loc) const;\n  void error(const Location& loc, const char* fmt, ...) const;\n  void warning(const Location& loc, const char* fmt, ...) const;\n  template<class... Args>\n  void error_context(const Location& loc, const char* fmt, Args&&... args) const {\n    error(loc, fmt, std::forward<Args>(args)...);\n    context(loc);\n  }\n  template<class... Args>\n  void warning_context(const Location& loc, const char* fmt, Args&&... args) const {\n    warning(loc, fmt, std::forward<Args>(args)...);\n    context(loc);\n  }\n};\n"
  },
  {
    "path": "yanshi/src/main.cc",
    "content": "#include \"common.hh\"\n#include \"fsa.hh\"\n#include \"loader.hh\"\n#include \"option.hh\"\n\n#include <errno.h>\n#include <getopt.h>\n#include <locale.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <string.h>\n#include <string>\n#include <sysexits.h>\n#include <unistd.h>\nusing namespace std;\n\nvoid print_help(FILE *fh)\n{\n  fprintf(fh, \"Usage: %s [OPTIONS] dir\\n\", program_invocation_short_name);\n  fputs(\n        \"\\n\"\n        \"Options:\\n\"\n        \"  -b,--bytes                make labels range over [0,256), Unicode literals will be treated as UTF-8 bytes\\n\"\n        \"  -C                        generate C source code (default: C++)\\n\"\n        \"  --check                   check syntax & use/def\\n\"\n        \"  --debug                   debug level\\n\"\n        \"  --debug-output            filename for debug output\\n\"\n        \"  --dump-action             dump associated actions for each edge\\n\"\n        \"  --dump-assoc              dump associated AST Expr for each state\\n\"\n        \"  --dump-automaton          dump automata\\n\"\n        \"  --dump-embed              dump statistics of EmbedExpr\\n\"\n        \"  --dump-module             dump module use/def/...\\n\"\n        \"  --dump-tree               dump AST\\n\"\n        \"  --extern-c                generate extern \\\"C\\\" specifier\\n\"\n        \"  -G,--graph <dir>          output a Graphviz dot file\\n\"\n        \"  -I,--import <dir>         add <dir> to search path for 'import'\\n\"\n        \"  -i,--interactive          interactive mode\\n\"\n        \"  --max-return-stack        max length of return stack in C generator (default: 100)\\n\"\n        \"  -k,--keep-inaccessible    do not perform accessible/co-accessible\\n\"\n        \"  -S,--standalone           generate header and 'main()'\\n\"\n        \"  --substring-grammar       construct regular approximation of the substring grammar. Inner states of nonterminals labeled 'intact' are not connected to start/final\\n\"\n        \"  -o,--output <file>        .cc output filename\\n\"\n        \"  -O,--output-header <file> .hh output filename\\n\"\n        \"  -h, --help                display this help and exit\\n\"\n        \"\\n\"\n        , fh);\n  exit(fh == stdout ? 0 : EX_USAGE);\n}\n\nint main(int argc, char *argv[])\n{\n  setlocale(LC_ALL, \"\");\n  int opt;\n  static struct option long_options[] = {\n    {\"bytes\",               no_argument,       0,   'b'},\n    {\"check\",               required_argument, 0,   'c'},\n    {\"debug\",               required_argument, 0,   'd'},\n    {\"debug-output\",        required_argument, 0,   'l'},\n    {\"dump-action\",         no_argument,       0,   1000},\n    {\"dump-assoc\",          no_argument,       0,   1001},\n    {\"dump-automaton\",      no_argument,       0,   1002},\n    {\"dump-embed\",          no_argument,       0,   1003},\n    {\"dump-module\",         no_argument,       0,   1004},\n    {\"dump-tree\",           no_argument,       0,   1005},\n    {\"extern-c\",            no_argument,       0,   1007},\n    {\"graph\",               no_argument,       0,   'G'},\n    {\"import\",              required_argument, 0,   'I'},\n    {\"interactive\",         no_argument,       0,   'i'},\n    {\"max-return-stack\",    required_argument, 0,   1006},\n    {\"keep-inaccessible\",   no_argument,       0,   'k'},\n    {\"standalone\",          no_argument,       0,   'S'},\n    {\"substring-grammar\",   no_argument,       0,   's'},\n    {\"output\",              required_argument, 0,   'o'},\n    {\"output-header\",       required_argument, 0,   'O'},\n    {\"help\",                no_argument,       0,   'h'},\n    {0,                     0,                 0,   0},\n  };\n\n  while ((opt = getopt_long(argc, argv, \"bCDcd:GhI:ikl:O:o:Ss\", long_options, NULL)) != -1) {\n    switch (opt) {\n    case 'b':\n      opt_bytes = true;\n      AB = 256;\n      break;\n    case 'C':\n      opt_gen_c = true;\n      break;\n    case 'D':\n      break;\n    case 'c':\n      opt_check = true;\n      break;\n    case 'd':\n      debug_level = get_long(optarg);\n      break;\n    case 'G':\n      opt_mode = Mode::graphviz;\n      break;\n    case 'h':\n      print_help(stdout);\n      break;\n    case 'I':\n      opt_include_paths.push_back(string(optarg));\n      break;\n    case 'i':\n      opt_mode = Mode::interactive;\n      break;\n    case 'k':\n      opt_keep_inaccessible = true;\n      break;\n    case 'l':\n      if (debug_file)\n        err_exit(EX_USAGE, \"multiple '-l'\");\n      debug_file = fopen(optarg, \"w\");\n      if (! debug_file)\n        err_exit(EX_OSFILE, \"fopen\");\n      break;\n    case 'O':\n      opt_output_header_filename = optarg;\n      break;\n    case 'o':\n      opt_output_filename = optarg;\n      break;\n    case 'S':\n      opt_standalone = true;\n      break;\n    case 's':\n      opt_substring_grammar = true;\n      break;\n    case 1000: opt_dump_action = true; break;\n    case 1001: opt_dump_assoc = true; break;\n    case 1002: opt_dump_automaton = true; break;\n    case 1003: opt_dump_embed = true; break;\n    case 1004: opt_dump_module = true; break;\n    case 1005: opt_dump_tree = true; break;\n    case 1006:\n      opt_max_return_stack = get_long(optarg);\n      break;\n    case 1007: opt_gen_extern_c = true; break;\n    case '?':\n      print_help(stderr);\n      break;\n    }\n  }\n  if (! debug_file)\n    debug_file = stderr;\n  argc -= optind;\n  argv += optind;\n\n  long n_errors = load(argc ? argv[0] : \"-\");\n  unload_all();\n  fclose(debug_file);\n  return n_errors ? 2 : 0;\n}\n"
  },
  {
    "path": "yanshi/src/option.cc",
    "content": "#include \"common.hh\"\n#include \"option.hh\"\n#include <stdio.h>\n\nbool opt_bytes, opt_check, opt_dump_action, opt_dump_assoc, opt_dump_automaton, opt_dump_embed, opt_dump_module, opt_dump_tree, opt_gen_c, opt_gen_extern_c, opt_keep_inaccessible, opt_standalone, opt_substring_grammar;\n\nlong AB = MAX_CODEPOINT+1, opt_max_return_stack = 100;\nlong debug_level = 3;\nFILE* debug_file;\nconst char* opt_output_filename = \"-\";\nconst char* opt_output_header_filename;\nMode opt_mode = Mode::cxx;\nvector<string> opt_include_paths;\n"
  },
  {
    "path": "yanshi/src/option.hh",
    "content": "#pragma once\n#include <string>\n#include <vector>\nusing std::string;\nusing std::vector;\n\nextern bool opt_bytes, opt_check, opt_dump_action, opt_dump_assoc, opt_dump_automaton, opt_dump_embed, opt_dump_module, opt_dump_tree, opt_gen_c, opt_gen_extern_c, opt_keep_inaccessible, opt_standalone, opt_substring_grammar;\nextern long AB, opt_max_return_stack;\nextern const char* opt_output_filename;\nextern const char* opt_output_header_filename;\nenum class Mode {cxx, graphviz, interactive};\nextern Mode opt_mode;\nextern vector<string> opt_include_paths;\n"
  },
  {
    "path": "yanshi/src/parser.y",
    "content": "%code requires {\n#include \"common.hh\"\n#include \"location.hh\"\n#include \"option.hh\"\n#include \"syntax.hh\"\n\n#include <limits.h>\n#include <unicode/utf8.h>\n\n#define YYINITDEPTH 1000\n#define YYLTYPE Location\n#define YYLLOC_DEFAULT(Loc, Rhs, N)             \\\n  do {                                          \\\n    if (N) {                                    \\\n      (Loc).start = YYRHSLOC(Rhs, 1).start;     \\\n      (Loc).end = YYRHSLOC(Rhs, N).end;         \\\n    } else {                                    \\\n      (Loc).start = YYRHSLOC(Rhs, 0).start;     \\\n      (Loc).end = YYRHSLOC(Rhs, 0).end;         \\\n    }                                           \\\n  } while (0)\n\nint parse(const LocationFile& locfile, Stmt*& res);\n}\n\n%locations\n%error-verbose\n%define api.pure\n\n%parse-param {Stmt*& res}\n%parse-param {long& errors}\n%parse-param {const LocationFile& locfile}\n%parse-param {void** lexer}\n%lex-param {Stmt*& res}\n%lex-param {long& errors}\n%lex-param {const LocationFile& locfile}\n%lex-param {void** lexer}\n\n%union {\n  long integer;\n  string* str;\n  DisjointIntervals* intervals;\n  Action* action;\n  Expr* expr;\n  Stmt* stmt;\n  char* errmsg;\n}\n%destructor { delete $$; } <str>\n%destructor { delete $$; } <action>\n%destructor { delete $$; } <expr>\n%destructor { delete $$; } <intervals>\n%destructor { delete $$; } <stmt>\n\n%token ACTION AMPERAMPER AS COLONCOLON CPP DOTDOT EPSILON EXPORT IMPORT INTACT INVALID_CHARACTER PREPROCESS_DEFINE\n%token <integer> CHAR INTEGER\n%token <str> IDENT\n%token <str> BRACED_CODE\n%token <str> STRING_LITERAL\n\n%type <action> action\n%type <expr> concat_expr difference_expr factor repeat intersect_expr union_expr union_expr2 unop_expr\n%type <intervals> bracket bracket_items\n%type <stmt> define_stmt preprocess stmt stmt_list\n\n%{\n#include \"lexer.hh\"\n\n#define FAIL(loc, errmsg)                                          \\\n  do {                                                             \\\n    Location l = loc;                                              \\\n    yyerror(&l, res, errors, locfile, lexer, errmsg);              \\\n  } while (0)\n\nvoid yyerror(YYLTYPE* loc, Stmt*& res, long& errors, const LocationFile& locfile, yyscan_t* lexer, const char *errmsg)\n{\n  errors++;\n  locfile.error_context(*loc, \"%s\", errmsg);\n}\n\nint yylex(YYSTYPE* yylval, YYLTYPE* loc, Stmt*& res, long& errors, const LocationFile& locfile, yyscan_t* lexer)\n{\n  int token = raw_yylex(yylval, loc, *lexer);\n  if (token == INVALID_CHARACTER) {\n    FAIL(*loc, yylval->errmsg ? yylval->errmsg : \"invalid character\");\n    free(yylval->errmsg);\n  }\n  return token;\n}\n\n#define gen_repeat(x, inner, low, high) \\\n  if (low < 0) {                     \\\n    FAIL(yyloc, \"negative\"); \\\n  } \\\n  if (low > high) { \\\n    FAIL(yyloc, \"low > high\"); \\\n  } \\\n  x = new RepeatExpr(inner, low, high)\n%}\n\n%%\n\ntoplevel:\n  stmt_list { res = $1; }\n\nstmt_list:\n    %empty { $$ = new EmptyStmt; }\n  | '\\n' stmt_list { $$ = $2; }\n  | stmt stmt_list { $1->next = $2; $2->prev = $1; $$ = $1; }\n  | error stmt_list { $$ = $2; }\n\nstmt:\n    define_stmt { $$ = $1; }\n  | preprocess '\\n' { $$ = $1; }\n  | IMPORT STRING_LITERAL AS IDENT '\\n' { $$ = new ImportStmt(*$2, *$4); delete $2; delete $4; $$->loc = yyloc; }\n  | IMPORT STRING_LITERAL '\\n' { string t; $$ = new ImportStmt(*$2, t); delete $2; $$->loc = yyloc; }\n  | ACTION IDENT BRACED_CODE '\\n' { $$ = new ActionStmt(*$2, *$3); delete $2; delete $3; $$->loc = yyloc; }\n  | CPP BRACED_CODE '\\n' { $$ = new CppStmt(*$2); delete $2; $$->loc = yyloc; }\n\npreprocess:\n    PREPROCESS_DEFINE IDENT INTEGER { $$ = new PreprocessDefineStmt(*$2, $3); delete $2; $$->loc = yyloc; }\n\neq:\n    '='\n  | ':'\n\ndefine_stmt:\n    IDENT eq union_expr '\\n' { $$ = new DefineStmt(*$1, $3); delete $1; $$->loc = yyloc; }\n  | IDENT eq '|' union_expr '\\n' { $$ = new DefineStmt(*$1, $4); delete $1; $$->loc = yyloc; }\n  | IDENT eq '\\n' union_expr2 '\\n' { $$ = new DefineStmt(*$1, $4); delete $1; $$->loc = yyloc; }\n  | IDENT eq '\\n' '|' union_expr2 '\\n' { $$ = new DefineStmt(*$1, $5); delete $1; $$->loc = yyloc; }\n  | EXPORT define_stmt { $$ = $2; ((DefineStmt*)$$)->export_ = true; $$->loc = yyloc; }\n  | EXPORT BRACED_CODE define_stmt { $$ = $3; ((DefineStmt*)$$)->export_ = true; ((DefineStmt*)$$)->export_params = *$2; delete $2; $$->loc = yyloc; }\n  | INTACT define_stmt { $$ = $2; ((DefineStmt*)$$)->intact = true; $$->loc = yyloc; }\n\nunion_expr:\n    intersect_expr { $$ = $1; }\n  | union_expr '|' intersect_expr { $$ = new UnionExpr($1, $3); $$->loc = yyloc; }\n\nunion_expr2:\n    intersect_expr { $$ = $1; }\n  | union_expr2 '|' intersect_expr { $$ = new UnionExpr($1, $3); $$->loc = yyloc; }\n  | union_expr2 '\\n' '|' intersect_expr { $$ = new UnionExpr($1, $4); $$->loc = yyloc; }\n\nintersect_expr:\n    difference_expr { $$ = $1; }\n  | intersect_expr AMPERAMPER difference_expr { $$ = new IntersectExpr($1, $3); $$->loc = yyloc; }\n\ndifference_expr:\n    concat_expr { $$ = $1; }\n  | difference_expr '-' concat_expr { $$ = new DifferenceExpr($1, $3); $$->loc = yyloc; }\n\nconcat_expr:\n    unop_expr { $$ = $1; }\n  | concat_expr unop_expr { $$ = new ConcatExpr($1, $2); $$->loc = yyloc; }\n\nunop_expr:\n    factor { $$ = $1; }\n  | '~' unop_expr { $$ = new ComplementExpr($2); $$->loc = yyloc; }\n\nfactor:\n    EPSILON { $$ = new EpsilonExpr; $$->loc = yyloc; }\n  | IDENT { string t; $$ = new EmbedExpr(t, *$1); delete $1; $$->loc = yyloc; }\n  | IDENT COLONCOLON IDENT { $$ = new EmbedExpr(*$1, *$3); delete $1; delete $3; $$->loc = yyloc; }\n  | '!' IDENT { string t; $$ = new CollapseExpr(t, *$2); delete $2; $$->loc = yyloc; }\n  | '!' IDENT COLONCOLON IDENT { $$ = new CollapseExpr(*$2, *$4); delete $2; delete $4; $$->loc = yyloc; }\n  | '&' IDENT { string t; $$ = new CallExpr(t, *$2); delete $2; $$->loc = yyloc; }\n  | '&' IDENT COLONCOLON IDENT { $$ = new CallExpr(*$2, *$4); delete $2; delete $4; $$->loc = yyloc; }\n  | STRING_LITERAL { $$ = new LiteralExpr(*$1); delete $1; $$->loc = yyloc; }\n  | '.' { $$ = new DotExpr(); $$->loc = yyloc; }\n  | INTEGER {\n      if (opt_bytes && 256 <= $1) {\n        FAIL(yyloc, \"literal integers should be less than 256 in bytes mode\");\n        $$ = new DotExpr;\n      } else {\n        auto t = new DisjointIntervals;\n        t->emplace($1, $1+1);\n        $$ = new BracketExpr(t);\n        $$->loc = yyloc;\n      }\n    }\n  | bracket { $$ = new BracketExpr($1); $$->loc = yyloc; }\n  | STRING_LITERAL DOTDOT STRING_LITERAL {\n      i32 c0, c1, i = 0, j = 0;\n      if (opt_bytes) {\n        c0 = u8((*$1)[0]);\n        c1 = u8((*$3)[0]);\n        i = j = 1;\n      } else {\n        U8_NEXT($1->c_str(), i, $1->size(), c0);\n        U8_NEXT($3->c_str(), j, $3->size(), c1);\n      }\n      delete $1;\n      delete $3;\n      if (i != $1->size() || j != $3->size()) {\n        FAIL(yyloc, \"endpoints of Unicode range should be of length 1\");\n        $$ = new DotExpr;\n      } else if (c0 > c1) {\n        FAIL(yyloc, \"negative Unicode range\");\n        $$ = new DotExpr;\n      } else {\n        auto t = new DisjointIntervals;\n        t->emplace(c0, c1+1);\n        $$ = new BracketExpr(t);\n        $$->loc = yyloc;\n      }\n    }\n  | '(' union_expr ')' { $$ = $2; }\n  | '(' error ')' { $$ = new DotExpr; }\n  | repeat { $$ = $1; }\n  | factor '>' action { $$ = $1; $$->entering.emplace_back($3, 0L); }\n  | factor '>' INTEGER action { $$ = $1; $$->entering.emplace_back($4, $3); }\n  | factor '@' action { $$ = $1; $$->finishing.emplace_back($3, 0L); }\n  | factor '@' INTEGER action { $$ = $1; $$->finishing.emplace_back($4, $3); }\n  | factor '%' action { $$ = $1; $$->leaving.emplace_back($3, 0L); }\n  | factor '%' INTEGER action { $$ = $1; $$->leaving.emplace_back($4, $3); }\n  | factor '$' action { $$ = $1; $$->transiting.emplace_back($3, 0L); }\n  | factor '$' INTEGER action { $$ = $1; $$->transiting.emplace_back($4, $3); }\n  | factor '+' { $$ = new PlusExpr($1); $$->loc = yyloc; }\n  | factor '?' { $$ = new QuestionExpr($1); $$->loc = yyloc; }\n  | factor '*' { $$ = new StarExpr($1); $$->loc = yyloc; }\n\nrepeat:\n    factor '{' INTEGER ',' INTEGER '}' { gen_repeat($$, $1, $3, $5); $$->loc = yyloc; }\n  | factor '{' INTEGER ',' '}' { gen_repeat($$, $1, $3, LONG_MAX); $$->loc = yyloc; }\n  | factor '{' INTEGER '}' { gen_repeat($$, $1, $3, $3); $$->loc = yyloc; }\n  | factor '{' ',' INTEGER '}' { gen_repeat($$, $1, 0, $4); $$->loc = yyloc; }\n\naction:\n    IDENT { string t; $$ = new RefAction(t, *$1); delete $1; $$->loc = yyloc; }\n  | IDENT COLONCOLON IDENT { $$ = new RefAction(*$1, *$3); delete $1; delete $3; $$->loc = yyloc; }\n  | BRACED_CODE { $$ = new InlineAction(*$1); delete $1; $$->loc = yyloc; }\n\nbracket:\n    '[' bracket_items ']' { $$ = $2; }\n  | '[' '^' bracket_items ']' {\n      $$ = $3;\n      $$->flip();\n    }\n\nbracket_items:\n    bracket_items CHAR '-' CHAR {\n      $$ = $1;\n      if ($2 > $4)\n        FAIL(yyloc, \"negative range in character class\");\n      else\n        $$->emplace($2, $4+1);\n    }\n  | bracket_items CHAR {\n      $$ = $1;\n      $$->emplace($2, $2+1);\n    }\n  | %empty { $$ = new DisjointIntervals; }\n\n%%\n\nint parse(const LocationFile& locfile, Stmt*& res)\n{\n  yyscan_t lexer;\n  raw_yylex_init_extra(0, &lexer);\n  YY_BUFFER_STATE buf = raw_yy_scan_bytes(locfile.data.c_str(), locfile.data.size(), lexer);\n  long errors = 0;\n  yyparse(res, errors, locfile, &lexer);\n  raw_yy_delete_buffer(buf, lexer);\n  raw_yylex_destroy(lexer);\n  if (errors > 0) {\n    stmt_free(res);\n    res = NULL;\n  }\n  return errors;\n}\n"
  },
  {
    "path": "yanshi/src/repl.cc",
    "content": "#include \"compiler.hh\"\n#include \"fsa_anno.hh\"\n#include \"loader.hh\"\n#include \"parser.hh\"\n#include \"lexer.hh\" // after parser.hh\n#include \"syntax.hh\"\n\n#include <algorithm>\n#include <assert.h>\n#include <functional>\n#include <inttypes.h>\n#include <sstream>\n#include <stdlib.h>\n#include <type_traits>\n#include <unicode/utf8.h>\n#include <unordered_map>\n#include <wctype.h>\n#ifdef HAVE_READLINE\n# include <readline/readline.h>\n# include <readline/history.h>\n#endif\nusing namespace std;\n\nenum class ReplMode {string, integer};\nstatic ReplMode mode = ReplMode::string;\nstatic const FsaAnno* anno;\nstatic bool quit;\n\nstruct Command\n{\n  const char* name;\n  function<void(const char*)> fn;\n} commands[] = {\n  {\".automaton\", [](const char*) {print_automaton(anno->fsa); }},\n  {\".assoc\", [](const char*) {print_assoc(*anno); }},\n  {\".help\",\n    [](const char*) {\n      fputs(\"Commands available from the prompt:\\n\"\n             \"  .automaton    dump automaton\\n\"\n             \"  .assoc        dump associated AST Expr for each state\\n\"\n             \"  .help         display this help\\n\"\n             \"  .integer      input is a list of non-negative integers, macros(#define) or '' \"\" quoted strings\\n\"\n             \"  .macro        display defined macros\\n\"\n             \"  .string       input is a string\\n\"\n             \"  .stmt <ident> change target DefineStmt to <ident>\\n\"\n             \"  .quit         exit interactive mode\\n\"\n             , stdout);\n    }},\n  {\".integer\",\n    [](const char*) {\n      mode = ReplMode::integer; puts(\".integer mode\");\n    }},\n  {\".macro\",\n    [](const char*) {\n      for (auto& it: main_module->macro)\n        printf(\"%s\\t%ld\\n\", it.first.c_str(), it.second->value);\n      for (auto* import: main_module->unqualified_import)\n        for (auto& it: import->macro)\n          printf(\"%s\\t%ld\\n\", it.first.c_str(), it.second->value);\n    }},\n  {\".stmt\",\n    [](const char* arg) {\n      Stmt* r = resolve(*main_module, \"\", arg);\n      if (! r)\n        printf(\"'%s' undefined\\n\", arg);\n      else if (r == (Stmt*)1)\n        printf(\"ambiguous '%s'\\n\", arg);\n      else if (auto d = dynamic_cast<PreprocessDefineStmt*>(r))\n        printf(\"'%s' is a macro\\n\", arg);\n      else if (auto d = dynamic_cast<DefineStmt*>(r)) {\n        anno = &compiled[d];\n        printf(\"%s :: DefineStmt\\n\", d->lhs.c_str());\n      } else\n        assert(0);\n    }},\n  {\".string\",\n    [](const char*) {\n      mode = ReplMode::string; puts(\".string mode\");\n    }\n  },\n  {\".quit\",\n    [](const char*) {\n      puts(\"Leaving interactive mode\");\n      quit = true;\n    }\n  },\n};\n\n#ifdef HAVE_READLINE\nstatic char* command_completer(const char* text, int state)\n{\n  static long i = 0;\n  if (! state)\n    i = 0;\n  while (i < LEN(commands)) {\n    Command* x = &commands[i++];\n    if (! strncmp(x->name, text, strlen(text)))\n      return strdup(x->name);\n  }\n  return NULL;\n}\n\nstatic char* macro_completer(const char* text, int state)\n{\n  static Stmt* x;\n  if (! state)\n    x = main_module->toplevel;\n  while (x) {\n    auto xx = dynamic_cast<PreprocessDefineStmt*>(x);\n    x = x->next;\n    if (xx && ! strncmp(xx->ident.c_str(), text, strlen(text)))\n      return strdup(xx->ident.c_str());\n  }\n  return NULL;\n}\n\nstatic char* stmt_completer(const char* text, int state)\n{\n  static Stmt* x;\n  if (! state)\n    x = main_module->toplevel;\n  while (x) {\n    auto xx = dynamic_cast<DefineStmt*>(x);\n    x = x->next;\n    if (xx && ! strncmp(xx->lhs.c_str(), text, strlen(text)))\n      return strdup(xx->lhs.c_str());\n  }\n  return NULL;\n}\n\nstatic char** on_complete(const char* text, int start, int end)\n{\n  rl_attempted_completion_over = 1;\n  if (! start)\n    return rl_completion_matches(text, command_completer);\n  if (6 <= start && ! strncmp(rl_line_buffer, \".stmt \", 6))\n    return rl_completion_matches(text, stmt_completer);\n  if (mode == ReplMode::integer)\n    return rl_completion_matches(text, macro_completer);\n  return NULL;\n}\n#else\nchar* readline(const char* prompt)\n{\n  char* r = NULL;\n  size_t s = 0;\n  ssize_t n;\n  fputs(prompt, stdout);\n  if ((n = getline(&r, &s, stdin)) > 0)\n    r[n-1] = '\\0';\n  else {\n    free(r);\n    r = NULL;\n  }\n  return r;\n}\n#endif\n\nstatic void run_command(char* line)\n{\n  size_t p = 1;\n  while (line[p] && isalnum(line[p]))\n    p++;\n  Command* com = NULL;\n  REP(i, LEN(commands))\n    if (! strncmp(commands[i].name, line, p)) {\n      if (com) com = (Command*)1;\n      else com = &commands[i];\n    }\n  if (! com)\n    printf(\"Unknown command '%s'\\n\", line);\n  else if (com == (Command*)1)\n    printf(\"Ambiguous command '%s'\\n\", line);\n  else {\n    while (line[p] && isspace(line[p]))\n      p++;\n    size_t len = strlen(line);\n    while (len && isspace(line[len-1]))\n      line[--len] = '\\0';\n    com->fn(line+p);\n  }\n}\n\nvoid repl(DefineStmt* stmt)\n{\n#ifdef HAVE_READLINE\n  rl_attempted_completion_function = on_complete;\n#endif\n  char buf[BUF_SIZE];\n  snprintf(buf, sizeof buf, \".stmt %s\", stmt->lhs.c_str());\n  run_command(buf);\n  strcpy(buf, \".integer\");\n  run_command(buf);\n  strcpy(buf, \".help\");\n  run_command(buf);\n  if (! anno) return;\n  char* line;\n  stringstream ss;\n  while ((line = readline(\"λ \")) != NULL) {\n#ifdef HAVE_READLINE\n    if (line[0])\n      add_history(line);\n#endif\n    if (line[0] == '.') {\n      run_command(line);\n      free(line);\n      if (quit) break;\n      continue;\n    }\n\n    long u = anno->fsa.start;\n\n    if (mode == ReplMode::string) {\n      i32 i = 0, len;\n      long c;\n      len = strlen(line);\n      if (anno->fsa.is_final(u)) yellow(1);\n      else normal_yellow(1);\n      printf(\"%ld \", u); sgr0();\n      while (i < len) {\n        U8_NEXT_OR_FFFD(line, i, len, c);\n        if (iswcntrl(c)) printf(\"%ld \", c);\n        else printf(\"%lc \", wint_t(c));\n        u = anno->fsa.transit(u, c);\n        if (anno->fsa.is_final(u)) yellow();\n        else normal_yellow();\n        printf(\"%ld \", u); sgr0();\n        if (u < 0) break;\n      }\n    } else {\n      vector<long> input;\n      int token;\n      yyscan_t lexer;\n      raw_yylex_init_extra(0, &lexer);\n      YY_BUFFER_STATE buf = raw_yy_scan_bytes(line, strlen(line), lexer);\n      YYSTYPE yylval;\n      YYLTYPE yylloc;\n      while (u >= 0 && (token = raw_yylex(&yylval, &yylloc, lexer)) != 0) {\n        switch (token) { // all tokens with a destructor should be listed\n        case IDENT: {\n          Stmt* stmt = resolve(*main_module, \"\", yylval.str->c_str());\n          if (! stmt) {\n            printf(\"'%s' undefined\", yylval.str->c_str());\n            u = -1;\n          } else if (stmt == (Stmt*)1) {\n            printf(\"ambiguous '%s'\", yylval.str->c_str());\n            u = -1;\n          } else if (auto d = dynamic_cast<PreprocessDefineStmt*>(stmt))\n            input.push_back(d->value);\n          else if (auto d = dynamic_cast<DefineStmt*>(stmt)) {\n            printf(\"'%s' is not a macro\", yylval.str->c_str());\n            u = -1;\n          } else\n            assert(0);\n          delete yylval.str;\n          break;\n        }\n        case INTEGER:\n          input.push_back(yylval.integer);\n          break;\n        case STRING_LITERAL:\n          if (opt_bytes)\n            for (unsigned char c: *yylval.str)\n              input.push_back(c);\n          else\n            for (i32 c, i = 0; i < yylval.str->size(); ) {\n              U8_NEXT_OR_FFFD(yylval.str->c_str(), i, yylval.str->size(), c);\n              input.push_back(c);\n            }\n          delete yylval.str;\n          break;\n        case BRACED_CODE:\n          delete yylval.str;\n          // fall through\n        default:\n          printf(\"invalid token at column %ld-%ld\\n\", yylloc.start+1, yylloc.end);\n          u = -1;\n          break;\n        }\n      }\n      raw_yy_delete_buffer(buf, lexer);\n      raw_yylex_destroy(lexer);\n\n      if (u >= 0) {\n        if (anno->fsa.is_final(u)) yellow(1);\n        else normal_yellow(1);\n        printf(\"%ld \", u); sgr0();\n        for (long c: input) {\n          printf(\"%ld \", c);\n          u = anno->fsa.transit(u, c);\n          if (anno->fsa.is_final(u)) yellow();\n          else normal_yellow();\n          printf(\"%ld \", u); sgr0();\n          if (u < 0) break;\n        }\n      }\n    }\n    free(line);\n    puts(\"\");\n    if (u >= 0) {\n      unordered_map<DefineStmt*, vector<long>> start_finals;\n      unordered_map<DefineStmt*, vector<pair<long, long>>> inners;\n      for (auto aa: anno->assoc[u]) {\n        if (has_start(aa.second))\n          start_finals[aa.first->stmt].push_back(aa.first->loc.start);\n        if (has_inner(aa.second)) {\n          inners[aa.first->stmt].emplace_back(aa.first->loc.start, 1);\n          inners[aa.first->stmt].emplace_back(aa.first->loc.end, -1);\n        }\n        if (has_final(aa.second))\n          start_finals[aa.first->stmt].push_back(aa.first->loc.end);\n      }\n      vector<DefineStmt*> stmts;\n      for (auto& it: start_finals)\n        stmts.push_back(it.first);\n      for (auto& it: inners)\n        stmts.push_back(it.first);\n      // sort DefineStmt by location\n      sort(ALL(stmts), [](const DefineStmt* x, const DefineStmt* y) {\n        if (x->module != y->module)\n          return x->module < y->module;\n        if (x->loc.start != y->loc.start)\n          return x->loc.start < y->loc.start;\n        return x->loc.end < y->loc.end;\n      });\n      stmts.erase(unique(ALL(stmts)), stmts.end());\n      for (auto* stmt: stmts) {\n        auto& start_final = start_finals[stmt];\n        auto& inner = inners[stmt];\n        sort(ALL(start_final));\n        sort(ALL(inner));\n        auto it0 = start_final.begin();\n        auto it1 = inner.begin();\n        long nest = 0;\n        FOR(i, stmt->loc.start, stmt->loc.end) {\n          for (; it0 != start_final.end() && *it0 < i; ++it0);\n          if (it0 != start_final.end() && *it0 == i) {\n            cyan();\n            putchar(':');\n            sgr0();\n          }\n          for (; it1 != inner.end() && it1->first <= i; ++it1)\n            nest += it1->second;\n          if (nest)\n            yellow();\n          putchar(stmt->module->locfile.data[i]);\n          if (nest)\n            sgr0();\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "yanshi/src/repl.hh",
    "content": "#pragma once\n#include \"syntax.hh\"\n\nvoid repl(DefineStmt* stmt);\n"
  },
  {
    "path": "yanshi/src/syntax.cc",
    "content": "#include \"syntax.hh\"\n\nvoid stmt_free(Stmt* stmt)\n{\n  while (stmt) {\n    auto x = stmt->next;\n    delete stmt;\n    stmt = x;\n  }\n}\n"
  },
  {
    "path": "yanshi/src/syntax.hh",
    "content": "#pragma once\n#include \"common.hh\"\n#include \"location.hh\"\n\n#include <cxxabi.h>\n#include <memory>\n#include <string.h>\n#include <string>\n#include <typeinfo>\n#include <vector>\nusing std::move;\nusing std::pair;\nusing std::string;\nusing std::vector;\n\n//// Visitor\n\ntemplate<class T>\nstruct Visitor;\n\ntemplate<class T>\nstruct VisitableBase {\n  virtual void accept(Visitor<T>& visitor) = 0;\n};\n\ntemplate<class Base, class Derived>\nstruct Visitable : Base {\n  void accept(Visitor<Base>& visitor) override {\n    visitor.visit(static_cast<Derived&>(*this));\n  }\n};\n\nstruct Action;\nstruct InlineAction;\nstruct RefAction;\ntemplate<>\nstruct Visitor<Action> {\n  virtual void visit(Action& action) = 0;\n  virtual void visit(InlineAction&) = 0;\n  virtual void visit(RefAction&) = 0;\n};\n\nstruct Expr;\nstruct BracketExpr;\nstruct CallExpr;\nstruct CollapseExpr;\nstruct ComplementExpr;\nstruct ConcatExpr;\nstruct DifferenceExpr;\nstruct DotExpr;\nstruct EmbedExpr;\nstruct EpsilonExpr;\nstruct IntersectExpr;\nstruct LiteralExpr;\nstruct PlusExpr;\nstruct RepeatExpr;\nstruct QuestionExpr;\nstruct StarExpr;\nstruct UnionExpr;\ntemplate<>\nstruct Visitor<Expr> {\n  virtual void visit(Expr&) = 0;\n  virtual void visit(BracketExpr&) = 0;\n  virtual void visit(CallExpr&) = 0;\n  virtual void visit(CollapseExpr&) = 0;\n  virtual void visit(ComplementExpr&) = 0;\n  virtual void visit(ConcatExpr&) = 0;\n  virtual void visit(DifferenceExpr&) = 0;\n  virtual void visit(DotExpr&) = 0;\n  virtual void visit(EmbedExpr&) = 0;\n  virtual void visit(EpsilonExpr&) = 0;\n  virtual void visit(IntersectExpr&) = 0;\n  virtual void visit(LiteralExpr&) = 0;\n  virtual void visit(PlusExpr&) = 0;\n  virtual void visit(RepeatExpr&) = 0;\n  virtual void visit(QuestionExpr&) = 0;\n  virtual void visit(StarExpr&) = 0;\n  virtual void visit(UnionExpr&) = 0;\n};\n\nstruct Stmt;\nstruct ActionStmt;\nstruct CppStmt;\nstruct DefineStmt;\nstruct EmptyStmt;\nstruct ImportStmt;\nstruct PreprocessDefineStmt;\ntemplate<>\nstruct Visitor<Stmt> {\n  virtual void visit(Stmt&) = 0;\n  virtual void visit(ActionStmt&) = 0;\n  virtual void visit(CppStmt&) = 0;\n  virtual void visit(DefineStmt&) = 0;\n  virtual void visit(EmptyStmt&) = 0;\n  virtual void visit(ImportStmt&) = 0;\n  virtual void visit(PreprocessDefineStmt&) = 0;\n};\n\n//// Action\n\nstruct Action : VisitableBase<Action> {\n  Location loc;\n  virtual ~Action() = default;\n};\n\nstruct InlineAction : Visitable<Action, InlineAction> {\n  string code;\n  InlineAction(string& code) : code(move(code)) {}\n};\n\nstruct Module;\nstruct RefAction : Visitable<Action, RefAction> {\n  string qualified, ident;\n  ActionStmt* define_stmt; // set by ModuleUse\n  RefAction(string& qualified, string& ident) : qualified(move(qualified)), ident(move(ident)) {}\n};\n\n//// Expr\n\nstruct Expr : VisitableBase<Expr> {\n  Location loc;\n  long pre, post, depth; // set by Compiler\n  vector<Expr*> anc; // set by Compiler\n  vector<pair<Action*, long>> entering, finishing, leaving, transiting;\n  DefineStmt* stmt = NULL; // set by ModuleImportDef\n  virtual ~Expr() {\n    for (auto a: entering)\n      delete a.first;\n    for (auto a: finishing)\n      delete a.first;\n    for (auto a: leaving)\n      delete a.first;\n    for (auto a: transiting)\n      delete a.first;\n  }\n  string name() const {\n    int status;\n    std::unique_ptr<char, void(*)(void*)> r{\n      abi::__cxa_demangle(typeid(*this).name(), NULL, NULL, &status),\n      free\n    };\n    std::string t = r.get();\n    t = t.substr(0, t.size()-4); // suffix 'Expr'\n    return t;\n  }\n  bool no_action() const {\n    return entering.empty() && finishing.empty() && leaving.empty() && transiting.empty();\n  }\n};\n\nstruct BracketExpr : Visitable<Expr, BracketExpr> {\n  DisjointIntervals intervals;\n  BracketExpr(DisjointIntervals* intervals) : intervals(std::move(*intervals)) { delete intervals; }\n};\n\nstruct CallExpr : Visitable<Expr, CallExpr> {\n  string qualified, ident;\n  DefineStmt* define_stmt = NULL; // set by ModuleUse\n  CallExpr(string& qualified, string& ident) : qualified(move(qualified)), ident(move(ident)) {}\n};\n\nstruct CollapseExpr : Visitable<Expr, CollapseExpr> {\n  string qualified, ident;\n  DefineStmt* define_stmt = NULL; // set by ModuleUse\n  CollapseExpr(string& qualified, string& ident) : qualified(move(qualified)), ident(move(ident)) {}\n};\n\nstruct ComplementExpr : Visitable<Expr, ComplementExpr> {\n  Expr* inner;\n  ComplementExpr(Expr* inner) : inner(inner) {}\n  ~ComplementExpr() {\n    delete inner;\n  }\n};\n\nstruct ConcatExpr : Visitable<Expr, ConcatExpr> {\n  Expr *lhs, *rhs;\n  ConcatExpr(Expr* lhs, Expr* rhs) : lhs(lhs), rhs(rhs) {}\n  ~ConcatExpr() {\n    delete lhs;\n    delete rhs;\n  }\n};\n\nstruct DifferenceExpr : Visitable<Expr, DifferenceExpr> {\n  Expr *lhs, *rhs;\n  DifferenceExpr(Expr* lhs, Expr* rhs) : lhs(lhs), rhs(rhs) {}\n  ~DifferenceExpr() {\n    delete lhs;\n    delete rhs;\n  }\n};\n\nstruct DotExpr : Visitable<Expr, DotExpr> {};\n\nstruct EmbedExpr : Visitable<Expr, EmbedExpr> {\n  string qualified, ident;\n  DefineStmt* define_stmt = NULL; // set by ModuleUse\n  long macro_value; // set by ModuleUse\n  EmbedExpr(string& qualified, string& ident) : qualified(move(qualified)), ident(move(ident)) {}\n};\n\nstruct EpsilonExpr : Visitable<Expr, EpsilonExpr> {};\n\nstruct IntersectExpr : Visitable<Expr, IntersectExpr> {\n  Expr *lhs, *rhs;\n  IntersectExpr(Expr* lhs, Expr* rhs) : lhs(lhs), rhs(rhs) {}\n  ~IntersectExpr() {\n    delete lhs;\n    delete rhs;\n  }\n};\n\nstruct LiteralExpr : Visitable<Expr, LiteralExpr> {\n  string literal;\n  LiteralExpr(string& literal) : literal(move(literal)) {}\n};\n\nstruct PlusExpr : Visitable<Expr, PlusExpr> {\n  Expr* inner;\n  PlusExpr(Expr* inner) : inner(inner) {}\n  ~PlusExpr() {\n    delete inner;\n  }\n};\n\nstruct RepeatExpr : Visitable<Expr, RepeatExpr> {\n  Expr* inner;\n  long low, high;\n  RepeatExpr(Expr* inner, long low, long high) : inner(inner), low(low), high(high) {}\n  ~RepeatExpr() {\n    delete inner;\n  }\n};\n\nstruct QuestionExpr : Visitable<Expr, QuestionExpr> {\n  Expr* inner;\n  QuestionExpr(Expr* inner) : inner(inner) {}\n  ~QuestionExpr() {\n    delete inner;\n  }\n};\n\nstruct StarExpr : Visitable<Expr, StarExpr> {\n  Expr* inner;\n  StarExpr(Expr* inner) : inner(inner) {}\n  ~StarExpr() {\n    delete inner;\n  }\n};\n\nstruct UnionExpr : Visitable<Expr, UnionExpr> {\n  Expr *lhs, *rhs;\n  UnionExpr(Expr* lhs, Expr* rhs) : lhs(lhs), rhs(rhs) {}\n  ~UnionExpr() {\n    delete lhs;\n    delete rhs;\n  }\n};\n\n//// Stmt\n\nstruct Stmt {\n  Location loc;\n  Stmt *prev = NULL, *next = NULL;\n  virtual ~Stmt() = default;\n  virtual void accept(Visitor<Stmt>& visitor) = 0;\n};\n\nstruct EmptyStmt : Visitable<Stmt, EmptyStmt> {};\n\nstruct ActionStmt : Visitable<Stmt, ActionStmt> {\n  string ident, code;\n  ActionStmt(string& ident, string& code) : ident(move(ident)), code(move(code)) {}\n};\n\nstruct CppStmt : Visitable<Stmt, CppStmt> {\n  string code;\n  CppStmt(string& code) : code(move(code)) {}\n};\n\nstruct DefineStmt : Visitable<Stmt, DefineStmt> {\n  bool export_ = false, intact = false;\n  string export_params, lhs;\n  Expr* rhs;\n  Module* module; // used in topological sort\n  DefineStmt(string& lhs, Expr* rhs) : lhs(move(lhs)), rhs(rhs) {}\n  ~DefineStmt() {\n    delete rhs;\n  }\n};\n\nstruct ImportStmt : Visitable<Stmt, ImportStmt> {\n  string filename, qualified;\n  ImportStmt(string& filename, string& qualified) : filename(move(filename)), qualified(move(qualified)) {}\n};\n\nstruct PreprocessDefineStmt : Visitable<Stmt, PreprocessDefineStmt> {\n  string ident;\n  long value;\n  PreprocessDefineStmt(string& ident, long value) : ident(move(ident)), value(value) {}\n};\n\nvoid stmt_free(Stmt* stmt);\n\n//// Visitor implementations\n\nstruct StmtPrinter : Visitor<Action>, Visitor<Expr>, Visitor<Stmt> {\n  int depth = 0;\n\n  void visit(Action& action) override {\n    action.accept(*this);\n  }\n  void visit(InlineAction& action) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"InlineAction\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", action.code.c_str());\n  }\n  void visit(RefAction& action) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"RefAction\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", action.ident.c_str());\n  }\n\n  void visit(Expr& expr) override {\n    if (expr.entering.size()) {\n      printf(\"%*s%s\\n\", 2*depth, \"\", \"@entering\");\n      depth++;\n      for (auto a: expr.entering) {\n        indent(stdout, depth);\n        printf(\"%ld\\n\", a.second);\n        a.first->accept(*this);\n      }\n      depth--;\n    }\n    if (expr.finishing.size()) {\n      printf(\"%*s%s\\n\", 2*depth, \"\", \"@finishing\");\n      depth++;\n      for (auto a: expr.finishing) {\n        indent(stdout, depth);\n        printf(\"%ld\\n\", a.second);\n        a.first->accept(*this);\n      }\n      depth--;\n    }\n    if (expr.leaving.size()) {\n      printf(\"%*s%s\\n\", 2*depth, \"\", \"@entering\");\n      depth++;\n      for (auto a: expr.leaving) {\n        indent(stdout, depth);\n        printf(\"%ld\\n\", a.second);\n        a.first->accept(*this);\n      }\n      depth--;\n    }\n    if (expr.transiting.size()) {\n      printf(\"%*s%s\\n\", 2*depth, \"\", \"@transiting\");\n      depth++;\n      for (auto a: expr.transiting) {\n        indent(stdout, depth);\n        printf(\"%ld\\n\", a.second);\n        a.first->accept(*this);\n      }\n      depth--;\n    }\n    expr.accept(*this);\n  }\n  void visit(BracketExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"BracketExpr\");\n    printf(\"%*s\", 2*(depth+1), \"\");\n    for (auto& x: expr.intervals.to)\n      printf(\"(%ld,%ld) \", x.first, x.second);\n    puts(\"\");\n  }\n  void visit(CallExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"CallExpr\");\n    printf(\"%*s\", 2*(depth+1), \"\");\n    if (expr.qualified.size())\n      printf(\"%s::%s\\n\", expr.qualified.c_str(), expr.ident.c_str());\n    else\n      printf(\"%s\\n\", expr.ident.c_str());\n  }\n  void visit(CollapseExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"CollapseExpr\");\n    printf(\"%*s\", 2*(depth+1), \"\");\n    if (expr.qualified.size())\n      printf(\"%s::%s\\n\", expr.qualified.c_str(), expr.ident.c_str());\n    else\n      printf(\"%s\\n\", expr.ident.c_str());\n  }\n  void visit(ComplementExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"ComplementExpr\");\n    depth++;\n    visit(*expr.inner);\n    depth--;\n  }\n  void visit(ConcatExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"ConcatExpr\");\n    depth++;\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n    depth--;\n  }\n  void visit(DifferenceExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"DifferenceExpr\");\n    depth++;\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n    depth--;\n  }\n  void visit(DotExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"DotExpr\");\n  }\n  void visit(EmbedExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"EmbedExpr\");\n    printf(\"%*s\", 2*(depth+1), \"\");\n    if (expr.qualified.size())\n      printf(\"%s::%s\\n\", expr.qualified.c_str(), expr.ident.c_str());\n    else\n      printf(\"%s\\n\", expr.ident.c_str());\n  }\n  void visit(EpsilonExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"EpsilonExpr\");\n  }\n  void visit(IntersectExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"IntersectExpr\");\n    depth++;\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n    depth--;\n  }\n  void visit(LiteralExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"LiteralExpr\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", expr.literal.c_str());\n  }\n  void visit(PlusExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"PlusExpr\");\n    depth++;\n    visit(*expr.inner);\n    depth--;\n  }\n  void visit(RepeatExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"RepeatExpr\");\n    printf(\"%*s%ld,%ld\\n\", 2*(depth+1), \"\", expr.low, expr.high);\n    depth++;\n    visit(*expr.inner);\n    depth--;\n  }\n  void visit(QuestionExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"QuestionExpr\");\n    depth++;\n    visit(*expr.inner);\n    depth--;\n  }\n  void visit(StarExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"StarExpr\");\n    depth++;\n    visit(*expr.inner);\n    depth--;\n  }\n  void visit(UnionExpr& expr) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"UnionExpr\");\n    depth++;\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n    depth--;\n  }\n\n  void visit(Stmt& stmt) override {\n    stmt.accept(*this);\n  }\n  void visit(ActionStmt& stmt) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"ActionStmt\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", stmt.ident.c_str());\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", stmt.code.c_str());\n  }\n  void visit(CppStmt& stmt) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"CppStmt\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", stmt.code.c_str());\n  }\n  void visit(DefineStmt& stmt) override {\n    printf(\"%*s%s%s\\n\", 2*depth, \"\", \"DefineStmt\", stmt.export_ ? \" export\" : \"\");\n    depth++;\n    indent(stdout, depth);\n    if (stmt.export_params.size())\n      printf(\"(%s) \", stmt.export_params.c_str());\n    printf(\"%s\\n\", stmt.lhs.c_str());\n    visit(*stmt.rhs);\n    depth--;\n  }\n  void visit(EmptyStmt& stmt) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"EmptyStmt\");\n  }\n  void visit(ImportStmt& stmt) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"ImportStmt\");\n    printf(\"%*s%s\\n\", 2*(depth+1), \"\", stmt.filename.c_str());\n    if (stmt.qualified.size())\n      printf(\"%*sas %s\\n\", 2*(depth+1), \"\", stmt.qualified.c_str());\n  }\n  void visit(PreprocessDefineStmt& stmt) override {\n    printf(\"%*s%s\\n\", 2*depth, \"\", \"PreprocessDefineStmt\");\n    printf(\"%*s%s %ld\\n\", 2*(depth+1), \"\", stmt.ident.c_str(), stmt.value);\n  }\n};\n\n//// Visitor implementation\n\nstruct PreorderStmtVisitor : Visitor<Stmt> {\n  void visit(Stmt& stmt) override { stmt.accept(*this); }\n  void visit(ActionStmt& stmt) override {}\n  void visit(CppStmt& stmt) override {}\n  void visit(DefineStmt& stmt) override {}\n  void visit(EmptyStmt& stmt) override {}\n  void visit(ImportStmt& stmt) override {}\n  void visit(PreprocessDefineStmt&) override {}\n};\n\nstruct PrePostActionExprStmtVisitor : Visitor<Action>, Visitor<Expr>, Visitor<Stmt> {\n  virtual void pre_action(Action&) {}\n  virtual void post_action(Action&) {}\n  virtual void pre_expr(Expr&) {}\n  virtual void post_expr(Expr&) {}\n  virtual void pre_stmt(Stmt&) {}\n  virtual void post_stmt(Stmt&) {}\n\n  void visit(Action& action) override {\n    pre_action(action);\n    action.accept(*this);\n    post_action(action);\n  }\n  void visit(InlineAction&) override {}\n  void visit(RefAction&) override {}\n\n  void visit(Expr& expr) override {\n    pre_expr(expr);\n    expr.accept(*this);\n    post_expr(expr);\n  }\n  void visit(BracketExpr& expr) override {}\n  void visit(CallExpr& expr) override {}\n  void visit(CollapseExpr& expr) override {}\n  void visit(ComplementExpr& expr) override { visit(*expr.inner); }\n  void visit(ConcatExpr& expr) override {\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n  }\n  void visit(DifferenceExpr& expr) override {\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n  }\n  void visit(DotExpr& expr) override {}\n  void visit(EmbedExpr& expr) override {}\n  void visit(EpsilonExpr& expr) override {}\n  void visit(IntersectExpr& expr) override {\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n  }\n  void visit(LiteralExpr& expr) override {}\n  void visit(PlusExpr& expr) override { visit(*expr.inner); }\n  void visit(RepeatExpr& expr) override { visit(*expr.inner); }\n  void visit(QuestionExpr& expr) override { visit(*expr.inner); }\n  void visit(StarExpr& expr) override { visit(*expr.inner); }\n  void visit(UnionExpr& expr) override {\n    visit(*expr.lhs);\n    visit(*expr.rhs);\n  }\n\n  void visit(Stmt& stmt) override {\n    pre_stmt(stmt);\n    stmt.accept(*this);\n    post_stmt(stmt);\n  }\n  void visit(ActionStmt& stmt) override {}\n  void visit(CppStmt& stmt) override {}\n  void visit(DefineStmt& stmt) override { stmt.rhs->accept(*this); }\n  void visit(EmptyStmt& stmt) override {}\n  void visit(ImportStmt& stmt) override {}\n  void visit(PreprocessDefineStmt&) override {}\n};\n"
  },
  {
    "path": "yanshi/unittest/determinize_test.cc",
    "content": "#include \"fsa.hh\"\n#include \"unittest/unittest_helper.hh\"\n\n#include <algorithm>\n#include <iostream>\n#include <type_traits>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\nusing namespace std;\n\nconst char test[] =\n\"4 7 2\\n\"\n\"2 3  \\n\"\n\"0 0 1\\n\"\n\"0 -1 2\\n\"\n\"1 1 1\\n\"\n\"1 1 3\\n\"\n\"2 -1 1\\n\"\n\"2 0 3\\n\"\n\"3 0 2\\n\"\n;\n\nint main(int argc, char *argv[])\n{\n  if (argc == 1) {\n    char filename[] = \"/tmp/XXXXXX\";\n    int fd = mkstemp(filename);\n    write(fd, test, sizeof test-1);\n    close(fd);\n    freopen(filename, \"r\", stdin);\n    unlink(filename);\n  }\n\n  auto relate = [](const vector<long>&) {};\n  Fsa fsa = read_nfa().determinize(relate);\n  print_fsa(fsa);\n\n  if (argc == 1)\n    return fsa.n() == 4 ? 0 : 1;\n}\n"
  },
  {
    "path": "yanshi/unittest/difference_test.cc",
    "content": "#include \"fsa.hh\"\n#include \"unittest/unittest_helper.hh\"\n\n#include <algorithm>\n#include <iostream>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\nusing namespace std;\n\nconst char test[] =\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 0 3\\n\"\n\"2 1 3\\n\"\n\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 1 3\\n\"\n\"2 1 3\\n\"\n;\n\nint main(int argc, char *argv[])\n{\n  if (argc == 1) {\n    char filename[] = \"/tmp/XXXXXX\";\n    int fd = mkstemp(filename);\n    write(fd, test, sizeof test-1);\n    close(fd);\n    freopen(filename, \"r\", stdin);\n    unlink(filename);\n  }\n\n  auto relate = [](long u, long v) {};\n  Fsa a = read_dfa(), b = read_dfa(), fsa = a.difference(b, relate);\n  print_fsa(fsa);\n\n  if (argc == 1)\n    return 0; // fsa.n() == 4 ? 0 : 1;\n}\n"
  },
  {
    "path": "yanshi/unittest/intersection_test.cc",
    "content": "#include \"fsa.hh\"\n#include \"unittest/unittest_helper.hh\"\n\n#include <algorithm>\n#include <iostream>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\nusing namespace std;\n\nconst char test[] =\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 0 3\\n\"\n\"2 1 3\\n\"\n\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 1 3\\n\"\n\"2 1 3\\n\"\n;\n\nint main(int argc, char *argv[])\n{\n  if (argc == 1) {\n    char filename[] = \"/tmp/XXXXXX\";\n    int fd = mkstemp(filename);\n    write(fd, test, sizeof test-1);\n    close(fd);\n    freopen(filename, \"r\", stdin);\n    unlink(filename);\n  }\n\n  auto relate = [](long u, long v) {};\n  Fsa a = read_dfa(), b = read_dfa(), fsa = a.intersect(b, relate);\n  print_fsa(fsa);\n\n  if (argc == 1)\n    return 0; // fsa.n() == 4 ? 0 : 1;\n}\n"
  },
  {
    "path": "yanshi/unittest/minimize_test.cc",
    "content": "#include \"fsa.hh\"\n#include \"unittest/unittest_helper.hh\"\n\n#include <algorithm>\n#include <iostream>\n#include <type_traits>\n#include <unistd.h>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\nusing namespace std;\n\nconst char test[] =\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 0 3\\n\"\n\"2 0 3\\n\"\n;\n\nint main(int argc, char *argv[])\n{\n  if (argc == 1) {\n    char filename[] = \"/tmp/XXXXXX\";\n    int fd = mkstemp(filename);\n    write(fd, test, sizeof test-1);\n    close(fd);\n    freopen(filename, \"r\", stdin);\n    unlink(filename);\n  }\n\n  auto relate = [](const vector<long>&) {};\n  Fsa fsa = read_dfa().minimize(relate);\n  print_fsa(fsa);\n\n  if (argc == 1)\n    return fsa.n() == 3 ? 0 : 1;\n}\n"
  },
  {
    "path": "yanshi/unittest/union_test.cc",
    "content": "#include \"fsa.hh\"\n#include \"unittest/unittest_helper.hh\"\n\n#include <algorithm>\n#include <iostream>\n#include <type_traits>\n#include <unistd.h>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\nusing namespace std;\n\nconst char test[] =\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 0 3\\n\"\n\"2 1 3\\n\"\n\n\"4 4 1\\n\"\n\"3  \\n\"\n\"0 0 1\\n\"\n\"0 1 2\\n\"\n\"1 1 3\\n\"\n\"2 1 3\\n\"\n;\n\nint main(int argc, char *argv[])\n{\n  if (argc == 1) {\n    char filename[] = \"/tmp/XXXXXX\";\n    int fd = mkstemp(filename);\n    write(fd, test, sizeof test-1);\n    close(fd);\n    freopen(filename, \"r\", stdin);\n    unlink(filename);\n  }\n\n  auto relate = [](long u, long v) {};\n  Fsa a = read_dfa(), b = read_dfa(), fsa = a.union_(b, relate);\n  print_fsa(fsa);\n\n  if (argc == 1)\n    return fsa.n() == 4 ? 0 : 1;\n}\n"
  },
  {
    "path": "yanshi/unittest/unittest_helper.hh",
    "content": "#pragma once\n#include \"common.hh\"\n#include \"fsa.hh\"\n\n#include <algorithm>\n#include <sysexits.h>\n#include <err.h>\n#include <assert.h>\n#include <iostream>\nusing namespace std;\n\nstatic Fsa read_nfa()\n{\n  long n, m, k, u, a, v;\n  cin >> n >> m >> k;\n  Fsa r;\n  r.start = 0;\n  r.adj.resize(n);\n  while (k--) {\n    cin >> u;\n    r.finals.push_back(u);\n  }\n  sort(ALL(r.finals));\n  while (m--) {\n    cin >> u >> a >> v;\n    if (u < 0 || u >= n)\n      errx(EX_DATAERR, \"%ld: 0 <= u < n\", u);\n    if (a < -1 || a >= 256)\n      errx(EX_DATAERR, \"%ld: -1 <= c < 256\", a);\n    if (v < 0 || v >= n)\n      errx(EX_DATAERR, \"%ld: 0 <= v < n\", v);\n    r.adj[u].emplace_back(a, v);\n  }\n  assert(cin.good());\n  REP(i, n)\n    sort(ALL(r.adj[i]));\n  return r;\n}\n\nstatic Fsa read_dfa()\n{\n  Fsa r = read_nfa();\n  REP(i, r.n())\n    if (r.adj[i].size()) {\n      if (r.adj[i][0].first < 0)\n        errx(EX_DATAERR, \"epsilon edge found for %ld\", i);\n      REP(j, r.adj[i].size()-1)\n        if (r.adj[i][j].first == r.adj[i][j+1].first)\n          errx(EX_DATAERR, \"duplicate labels %ld found for %ld\", r.adj[i][j].first, i);\n    }\n  assert(cin.good());\n  return r;\n}\n\nstatic void print_fsa(const Fsa& fsa)\n{\n  printf(\"finals:\");\n  for (long i: fsa.finals)\n    printf(\" %ld\", i);\n  puts(\"\");\n  puts(\"edges:\");\n  REP(i, fsa.n()) {\n    printf(\"%ld:\", i);\n    for (auto& x: fsa.adj[i])\n      printf(\" (%ld,%ld)\", x.first, x.second);\n    puts(\"\");\n  }\n}\n"
  }
]