diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cb3b7e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +############################### +# Core EditorConfig Options # +############################### + +root = true + +# All files +[*] +indent_style = space + +# Code files +[*.cs] +indent_size = 4 +insert_final_newline = true +charset = utf-8 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..81e4b6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +#* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +#*.c text +#*.h text + +# Declare files that will always have CRLF line endings on checkout. +#*.sln text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.DAT binary +*.ico binary +*.jpg binary +*.exe binary +*.dll binary +*.pfx binary \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..160740e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ba350df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Please describe the idea.** +A clear and concise description of the features functionality is. Ex. I'd like to have a command that [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..73c045a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,13 @@ +name: Build +on: [pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + - name: Build Project + run: dotnet build --configuration Release \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3de6199 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: Create Release +on: + push: + branches: + - master +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v1 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + + - name: Build Release + id: build_release + run: | + MHF_VERSION_FILE=${{ github.workspace }}/mhf.version + MHF_VERSION=$(<"$MHF_VERSION_FILE") + echo ::set-env name=MHF_VERSION::$MHF_VERSION + echo ::set-env name=MHF_VERSION_E::$(echo ${GITHUB_SHA} | cut -c1-8) + mkdir ./release + for MHF_RUNTIME in win-x86 win-x64 linux-x64 osx-x64; do + # Server + dotnet publish Mhf.Cli/Mhf.Cli.csproj /p:Version=$MHF_VERSION /p:FromMSBuild=true --runtime $MHF_RUNTIME --configuration Release --output ./publish/$MHF_RUNTIME-$MHF_VERSION/Server + # ReleaseFiles + cp -r ./ReleaseFiles/. ./publish/$MHF_RUNTIME-$MHF_VERSION/ + # Pack + tar cjf ./release/$MHF_RUNTIME-$MHF_VERSION.tar.gz ./publish/$MHF_RUNTIME-$MHF_VERSION + done + + - name: Create Release + id: create_release + uses: actions/create-release@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: release-${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }} + release_name: Release ${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }} + draft: false + prerelease: false + + - name: Upload win-x86 Release Asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./release/win-x86-${{ env.MHF_VERSION }}.tar.gz + asset_name: win-x86-${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }}.tar.gz + asset_content_type: application/gzip + + - name: Upload win-x64 Release Asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./release/win-x64-${{ env.MHF_VERSION }}.tar.gz + asset_name: win-x64-${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }}.tar.gz + asset_content_type: application/gzip + + - name: Upload linux-x64 Release Asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./release/linux-x64-${{ env.MHF_VERSION }}.tar.gz + asset_name: linux-x64-${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }}.tar.gz + asset_content_type: application/gzip + + - name: Upload osx-x64 Release Asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./release/osx-x64-${{ env.MHF_VERSION }}.tar.gz + asset_name: osx-x64-${{ env.MHF_VERSION }}-${{ env.MHF_VERSION_E }}.tar.gz + asset_content_type: application/gzip \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc12c96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +## Folders specific +/publish/ +/release/ +/packages/ + +## Folders anywhere +.idea/ +.vs/ + +## Files anywhere +*.DS_Store + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/Mhf.Cli/Argument/ConsoleParameter.cs b/Mhf.Cli/Argument/ConsoleParameter.cs new file mode 100644 index 0000000..43e31d9 --- /dev/null +++ b/Mhf.Cli/Argument/ConsoleParameter.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Mhf.Cli.Argument +{ + public class ConsoleParameter + { + public ConsoleParameter(string key) + { + Key = key; + Arguments = new List(); + Switches = new List(); + ArgumentMap = new Dictionary(); + SwitchMap = new Dictionary(); + } + + public string Key { get; } + public List Arguments { get; } + public List Switches { get; } + public Dictionary SwitchMap { get; } + public Dictionary ArgumentMap { get; } + + public void Clear() + { + Arguments.Clear(); + Switches.Clear(); + ArgumentMap.Clear(); + SwitchMap.Clear(); + } + } +} diff --git a/Mhf.Cli/Argument/ISwitchConsumer.cs b/Mhf.Cli/Argument/ISwitchConsumer.cs new file mode 100644 index 0000000..982cb31 --- /dev/null +++ b/Mhf.Cli/Argument/ISwitchConsumer.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Mhf.Cli.Argument +{ + public interface ISwitchConsumer + { + List Switches { get; } + } +} diff --git a/Mhf.Cli/Argument/ISwitchProperty.cs b/Mhf.Cli/Argument/ISwitchProperty.cs new file mode 100644 index 0000000..c9e0045 --- /dev/null +++ b/Mhf.Cli/Argument/ISwitchProperty.cs @@ -0,0 +1,10 @@ +namespace Mhf.Cli.Argument +{ + public interface ISwitchProperty + { + string Key { get; } + string Description { get; } + string ValueDescription { get; } + bool Assign(string value); + } +} diff --git a/Mhf.Cli/Argument/SwitchProperty.cs b/Mhf.Cli/Argument/SwitchProperty.cs new file mode 100644 index 0000000..4f113f6 --- /dev/null +++ b/Mhf.Cli/Argument/SwitchProperty.cs @@ -0,0 +1,48 @@ +namespace Mhf.Cli.Argument +{ + public class SwitchProperty : ISwitchProperty + { + public static TryParseHandler NoOp = (string value, out T result) => + { + result = default; + return true; + }; + + private TryParseHandler _parser; + private AssignHandler _assigner; + + public SwitchProperty(string key, string valueDescription, string description, TryParseHandler parser, + AssignHandler assigner) + { + Key = key; + ValueDescription = valueDescription; + Description = description; + _parser = parser; + _assigner = assigner; + } + + public delegate bool TryParseHandler(string value, out T result); + + public delegate void AssignHandler(T result); + + public string Key { get; } + public string Description { get; } + public string ValueDescription { get; } + + public bool Assign(string value) + { + if (string.IsNullOrEmpty(value)) + { + return false; + } + + if (!_parser(value, out T result)) + { + return false; + } + + _assigner(result); + return true; + } + } +} diff --git a/Mhf.Cli/Command/CommandResultType.cs b/Mhf.Cli/Command/CommandResultType.cs new file mode 100644 index 0000000..e5d4ad6 --- /dev/null +++ b/Mhf.Cli/Command/CommandResultType.cs @@ -0,0 +1,9 @@ +namespace Mhf.Cli.Command +{ + public enum CommandResultType + { + Exit, + Continue, + Completed + } +} diff --git a/Mhf.Cli/Command/Commands/DecryptCommand.cs b/Mhf.Cli/Command/Commands/DecryptCommand.cs new file mode 100644 index 0000000..54264b2 --- /dev/null +++ b/Mhf.Cli/Command/Commands/DecryptCommand.cs @@ -0,0 +1,19 @@ +using Mhf.Cli.Argument; +using Mhf.Server.Packet; +using Mhf.Server.Setting; + +namespace Mhf.Cli.Command.Commands +{ + public class DecryptCommand : ConsoleCommand + { + public override CommandResultType Handle(ConsoleParameter parameter) + { + PacketFactory pf = new PacketFactory(new MhfSetting()); + pf.Test(); + return CommandResultType.Completed; + } + + public override string Key => "decrypt"; + public override string Description => "Decrypt packet data"; + } +} diff --git a/Mhf.Cli/Command/Commands/ExitCommand.cs b/Mhf.Cli/Command/Commands/ExitCommand.cs new file mode 100644 index 0000000..e70ca5e --- /dev/null +++ b/Mhf.Cli/Command/Commands/ExitCommand.cs @@ -0,0 +1,16 @@ +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command.Commands +{ + public class ExitCommand : ConsoleCommand + { + public override CommandResultType Handle(ConsoleParameter parameter) + { + Logger.Info("Exiting..."); + return CommandResultType.Exit; + } + + public override string Key => "exit"; + public override string Description => "Closes the program"; + } +} diff --git a/Mhf.Cli/Command/Commands/HelpCommand.cs b/Mhf.Cli/Command/Commands/HelpCommand.cs new file mode 100644 index 0000000..0d059db --- /dev/null +++ b/Mhf.Cli/Command/Commands/HelpCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command.Commands +{ + public class HelpCommand : ConsoleCommand + { + private readonly Dictionary _commands; + + public HelpCommand(Dictionary commands) + { + _commands = commands; + } + + public override CommandResultType Handle(ConsoleParameter parameter) + { + if (parameter.Arguments.Count >= 1) + { + string subKey = parameter.Arguments[0]; + if (!_commands.ContainsKey(subKey)) + { + Logger.Error( + $"Command: 'help {subKey}' not available. Type 'help' for a list of available commands."); + return CommandResultType.Continue; + } + + IConsoleCommand consoleCommandHelp = _commands[subKey]; + Logger.Info(ShowHelp(consoleCommandHelp)); + return CommandResultType.Continue; + } + + ShowHelp(); + return CommandResultType.Continue; + } + + private void ShowHelp() + { + StringBuilder sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.Append("Available Commands:"); + foreach (string key in _commands.Keys) + { + sb.Append(Environment.NewLine); + sb.Append("----------"); + IConsoleCommand command = _commands[key]; + sb.Append(ShowHelp(command)); + } + + Logger.Info(sb.ToString()); + } + + private string ShowHelp(IConsoleCommand command) + { + StringBuilder sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.Append(command.Key); + sb.Append(Environment.NewLine); + sb.Append($"- {command.Description}"); + return sb.ToString(); + } + + public override string Key => "help"; + public override string Description => "Displays this text"; + } +} diff --git a/Mhf.Cli/Command/Commands/ServerCommand.cs b/Mhf.Cli/Command/Commands/ServerCommand.cs new file mode 100644 index 0000000..59327c5 --- /dev/null +++ b/Mhf.Cli/Command/Commands/ServerCommand.cs @@ -0,0 +1,55 @@ +using System; +using Mhf.Cli.Argument; +using Mhf.Server; +using Mhf.Server.Setting; + +namespace Mhf.Cli.Command.Commands +{ + public class ServerCommand : ConsoleCommand + { + private MhfServer _server; + private readonly LogWriter _logWriter; + + public ServerCommand(LogWriter logWriter) + { + _logWriter = logWriter; + } + + public override void Shutdown() + { + if (_server != null) + { + _server.Stop(); + } + } + + public override CommandResultType Handle(ConsoleParameter parameter) + { + if (_server == null) + { + MhfSetting setting = new MhfSetting(); + _server = new MhfServer(setting); + } + + if (parameter.Arguments.Contains("start")) + { + _server.Start(); + return CommandResultType.Completed; + } + + if (parameter.Arguments.Contains("stop")) + { + _server.Stop(); + return CommandResultType.Completed; + } + + return CommandResultType.Continue; + } + + public override string Key => "server"; + + + public override string Description => + $"Monster Hunter Frontier Z Online Server. Ex.:{Environment.NewLine}server start{Environment.NewLine}server stop"; + } +} diff --git a/Mhf.Cli/Command/Commands/ShowCommand.cs b/Mhf.Cli/Command/Commands/ShowCommand.cs new file mode 100644 index 0000000..a081820 --- /dev/null +++ b/Mhf.Cli/Command/Commands/ShowCommand.cs @@ -0,0 +1,98 @@ +using System; +using System.Text; +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command.Commands +{ + public class ShowCommand : ConsoleCommand + { + public override CommandResultType Handle(ConsoleParameter parameter) + { + if (parameter.Arguments.Contains("w")) + { + StringBuilder sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.Append("15. Disclaimer of Warranty."); + sb.Append(Environment.NewLine); + sb.Append("THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY"); + sb.Append(Environment.NewLine); + sb.Append("APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT"); + sb.Append(Environment.NewLine); + sb.Append("HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY"); + sb.Append(Environment.NewLine); + sb.Append("OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,"); + sb.Append(Environment.NewLine); + sb.Append("THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR"); + sb.Append(Environment.NewLine); + sb.Append("PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM"); + sb.Append(Environment.NewLine); + sb.Append("IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF"); + sb.Append(Environment.NewLine); + sb.Append("ALL NECESSARY SERVICING, REPAIR OR CORRECTION."); + sb.Append(Environment.NewLine); + Logger.Info(sb.ToString()); + return CommandResultType.Completed; + } + + if (parameter.Arguments.Contains("c")) + { + StringBuilder sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.Append("2. Basic Permissions."); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append("All rights granted under this License are granted for the term of"); + sb.Append(Environment.NewLine); + sb.Append("copyright on the Program, and are irrevocable provided the stated"); + sb.Append(Environment.NewLine); + sb.Append("conditions are met. This License explicitly affirms your unlimited"); + sb.Append(Environment.NewLine); + sb.Append("permission to run the unmodified Program. The output from running a"); + sb.Append(Environment.NewLine); + sb.Append("covered work is covered by this License only if the output, given its"); + sb.Append(Environment.NewLine); + sb.Append("content, constitutes a covered work. This License acknowledges your"); + sb.Append(Environment.NewLine); + sb.Append("rights of fair use or other equivalent, as provided by copyright law."); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append("You may make, run and propagate covered works that you do not"); + sb.Append(Environment.NewLine); + sb.Append("convey, without conditions so long as your license otherwise remains"); + sb.Append(Environment.NewLine); + sb.Append("in force. You may convey covered works to others for the sole purpose"); + sb.Append(Environment.NewLine); + sb.Append("of having them make modifications exclusively for you, or provide you"); + sb.Append(Environment.NewLine); + sb.Append("with facilities for running those works, provided that you comply with"); + sb.Append(Environment.NewLine); + sb.Append("the terms of this License in conveying all material for which you do"); + sb.Append(Environment.NewLine); + sb.Append("not control copyright. Those thus making or running the covered works"); + sb.Append(Environment.NewLine); + sb.Append("for you must do so exclusively on your behalf, under your direction"); + sb.Append(Environment.NewLine); + sb.Append("and control, on terms that prohibit them from making any copies of"); + sb.Append(Environment.NewLine); + sb.Append("your copyrighted material outside their relationship with you."); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append("Conveying under any other circumstances is permitted solely under"); + sb.Append(Environment.NewLine); + sb.Append("the conditions stated below. Sublicensing is not allowed; section 10"); + sb.Append(Environment.NewLine); + sb.Append("makes it unnecessary."); + sb.Append(Environment.NewLine); + Logger.Info(sb.ToString()); + return CommandResultType.Completed; + } + + return CommandResultType.Continue; + } + + public override string Key => "show"; + + public override string Description => + $"Shows Copyright. Ex.:{Environment.NewLine}show w{Environment.NewLine}show c"; + } +} diff --git a/Mhf.Cli/Command/Commands/SwitchCommand.cs b/Mhf.Cli/Command/Commands/SwitchCommand.cs new file mode 100644 index 0000000..121fa57 --- /dev/null +++ b/Mhf.Cli/Command/Commands/SwitchCommand.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command.Commands +{ + public class SwitchCommand : ConsoleCommand + { + private readonly List _parameterConsumers; + + public SwitchCommand(List parameterConsumers) + { + _parameterConsumers = parameterConsumers; + } + + public override CommandResultType Handle(ConsoleParameter parameter) + { + foreach (string key in parameter.SwitchMap.Keys) + { + ISwitchProperty property = FindSwitch(key); + if (property == null) + { + Logger.Error($"Switch '{key}' not found"); + continue; + } + + string value = parameter.SwitchMap[key]; + if (!property.Assign(value)) + { + Logger.Error($"Switch '{key}' failed, value: '{value}' is invalid"); + } + else + { + Logger.Info($"Applied {key}={value}"); + } + } + + foreach (string booleanSwitch in parameter.Switches) + { + ISwitchProperty property = FindSwitch(booleanSwitch); + if (property == null) + { + Logger.Error($"Switch '{booleanSwitch}' not found"); + continue; + } + if (!property.Assign(bool.TrueString)) + { + Logger.Error($"Switch '{booleanSwitch}' failed, value: '{bool.TrueString}' is invalid"); + } + else + { + Logger.Info($"Applied {booleanSwitch}={bool.TrueString}"); + } + } + + return CommandResultType.Completed; + } + + private ISwitchProperty FindSwitch(string key) + { + foreach (ISwitchConsumer consumer in _parameterConsumers) + { + foreach (ISwitchProperty property in consumer.Switches) + { + if (property.Key == key) + { + return property; + } + } + } + + return null; + } + + private string ShowSwitches() + { + StringBuilder sb = new StringBuilder(); + sb.Append("Available Switches:"); + sb.Append(Environment.NewLine); + foreach (ISwitchConsumer consumer in _parameterConsumers) + { + foreach (ISwitchProperty property in consumer.Switches) + { + sb.Append(Environment.NewLine); + sb.Append(property.Key); + sb.Append(Environment.NewLine); + sb.Append("> Ex.: "); + sb.Append(property.ValueDescription); + sb.Append(Environment.NewLine); + sb.Append("> "); + sb.Append(property.Description); + sb.Append(Environment.NewLine); + } + } + + return sb.ToString(); + } + + public override string Key => "switch"; + public override string Description => $"Changes configuration switches{Environment.NewLine}{ShowSwitches()}"; + } +} diff --git a/Mhf.Cli/Command/ConsoleCommand.cs b/Mhf.Cli/Command/ConsoleCommand.cs new file mode 100644 index 0000000..c9110bb --- /dev/null +++ b/Mhf.Cli/Command/ConsoleCommand.cs @@ -0,0 +1,23 @@ +using Arrowgene.Services.Logging; +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command +{ + public abstract class ConsoleCommand : IConsoleCommand + { + protected ConsoleCommand() + { + Logger = LogProvider.Logger(this); + } + + protected readonly ILogger Logger; + + public abstract string Key { get; } + public abstract string Description { get; } + public abstract CommandResultType Handle(ConsoleParameter parameter); + + public virtual void Shutdown() + { + } + } +} diff --git a/Mhf.Cli/Command/IConsoleCommand.cs b/Mhf.Cli/Command/IConsoleCommand.cs new file mode 100644 index 0000000..40c7de4 --- /dev/null +++ b/Mhf.Cli/Command/IConsoleCommand.cs @@ -0,0 +1,12 @@ +using Mhf.Cli.Argument; + +namespace Mhf.Cli.Command +{ + public interface IConsoleCommand + { + CommandResultType Handle(ConsoleParameter parameter); + void Shutdown(); + string Key { get; } + string Description { get; } + } +} diff --git a/Mhf.Cli/LogWriter.cs b/Mhf.Cli/LogWriter.cs new file mode 100644 index 0000000..ff47a29 --- /dev/null +++ b/Mhf.Cli/LogWriter.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading; +using Arrowgene.Services.Buffers; +using Arrowgene.Services.Logging; +using Mhf.Cli.Argument; +using Mhf.Server.Logging; + +namespace Mhf.Cli +{ + public class LogWriter : ISwitchConsumer + { + private readonly object _consoleLock; + private readonly HashSet _packetIdWhitelist; + private readonly HashSet _packetIdBlacklist; + private readonly ILogger _logger; + private readonly Queue _logQueue; + private bool _paused; + private bool _continueing; + + public LogWriter() + { + _logger = LogProvider.Logger(this); + _packetIdWhitelist = new HashSet(); + _packetIdBlacklist = new HashSet(); + _logQueue = new Queue(); + _consoleLock = new object(); + Switches = new List(); + _paused = false; + _continueing = false; + Reset(); + LoadSwitches(); + LogProvider.GlobalLogWrite += LogProviderOnGlobalLogWrite; + } + + public List Switches { get; } + + /// + /// --max-packet-size=64 + /// + public int MaxPacketSize { get; set; } + + /// + /// --no-data=true + /// + public bool NoData { get; set; } + + /// + /// --log-level=2 + /// + public int MinLogLevel { get; set; } + + public void Reset() + { + MaxPacketSize = -1; + NoData = false; + MinLogLevel = (int) LogLevel.Debug; + _packetIdWhitelist.Clear(); + _packetIdBlacklist.Clear(); + } + + public void WhitelistPacket(ushort packetId) + { + if (_packetIdWhitelist.Contains(packetId)) + { + _logger.Error($"PacketId:{packetId} is already whitelisted"); + return; + } + + _packetIdWhitelist.Add(packetId); + } + + public void BlacklistPacket(ushort packetId) + { + if (_packetIdBlacklist.Contains(packetId)) + { + _logger.Error($"PacketId:{packetId} is already blacklisted"); + return; + } + + _packetIdBlacklist.Add(packetId); + } + + public void Pause() + { + _paused = true; + } + + public void Continue() + { + _continueing = true; + while (_logQueue.TryDequeue(out Log log)) + { + WriteLog(log); + } + + _paused = false; + _continueing = false; + } + + private void LoadSwitches() + { + Switches.Add( + new SwitchProperty( + "--no-data", + "--no-data=true (true|false)", + "Don't display packet data", + bool.TryParse, + (result => NoData = result) + ) + ); + Switches.Add( + new SwitchProperty( + "--max-packet-size", + "--max-packet-size=64 (integer)", + "Don't display packet data", + int.TryParse, + (result => MaxPacketSize = result) + ) + ); + Switches.Add( + new SwitchProperty( + "--log-level", + "--log-level=20 (integer) [Debug=10, Info=20, Error=30]", + "Only display logs of the same level or above", + int.TryParse, + (result => MinLogLevel = result) + ) + ); + Switches.Add( + new SwitchProperty( + "--clear", + "--clear", + "Resets all switches to default", + SwitchProperty.NoOp, + result => Reset() + ) + ); + Switches.Add( + new SwitchProperty>( + "--b-list", + "--b-list=1000,2000,0xAA (PacketId[0xA|10])", + "A blacklist that does not logs packets specified", + TryParsePacketIdList, + results => { AssinPacketIdList(results, BlacklistPacket); } + ) + ); + Switches.Add( + new SwitchProperty>( + "--w-list", + "--w-list=1000,2000,0xAA (PacketId[0xA|10])", + "A whitelist that only logs packets specified", + TryParsePacketIdList, + results => { AssinPacketIdList(results, WhitelistPacket); } + ) + ); + } + + /// + /// parses strings like "1:1000,2000,3:4000" into ServerType and PacketId + /// + private bool TryParsePacketIdList(string value, out List result) + { + if (string.IsNullOrEmpty(value)) + { + result = null; + return false; + } + + string[] values = value.Split(","); + + result = new List(); + foreach (string entry in values) + { + NumberStyles numberStyles; + String entryStr; + if (entry.StartsWith("0x")) + { + entryStr = entry.Substring(2); + numberStyles = NumberStyles.HexNumber; + } + else + { + entryStr = entry; + numberStyles = NumberStyles.Integer; + } + + + if (!ushort.TryParse(entryStr, numberStyles, null, out ushort val)) + { + return false; + } + + result.Add(val); + } + + return true; + } + + private void AssinPacketIdList(List results, Action addToPacketList) + { + foreach (ushort entry in results) + { + addToPacketList(entry); + } + } + + private void LogProviderOnGlobalLogWrite(object sender, LogWriteEventArgs logWriteEventArgs) + { + while (_continueing) + { + Thread.Sleep(10000); + } + + if (_paused) + { + _logQueue.Enqueue(logWriteEventArgs.Log); + return; + } + + WriteLog(logWriteEventArgs.Log); + } + + private void WriteLog(Log log) + { + ConsoleColor consoleColor; + string text; + + object tag = log.Tag; + if (tag is MhfLogPacket logPacket) + { + switch (logPacket.LogType) + { + case MhfLogType.PacketIn: + consoleColor = ConsoleColor.Green; + break; + case MhfLogType.PacketOut: + consoleColor = ConsoleColor.Blue; + break; + case MhfLogType.PacketUnhandled: + consoleColor = ConsoleColor.Red; + break; + default: + consoleColor = ConsoleColor.Gray; + break; + } + + text = CreatePacketLog(logPacket); + } + else + { + LogLevel logLevel = log.LogLevel; + if ((int) logLevel < MinLogLevel) + { + return; + } + + switch (logLevel) + { + case LogLevel.Debug: + consoleColor = ConsoleColor.DarkCyan; + break; + case LogLevel.Info: + consoleColor = ConsoleColor.Cyan; + break; + case LogLevel.Error: + consoleColor = ConsoleColor.Red; + break; + default: + consoleColor = ConsoleColor.Gray; + break; + } + + text = log.ToString(); + } + + if (text == null) + { + return; + } + + lock (_consoleLock) + { + Console.ForegroundColor = consoleColor; + Console.WriteLine(text); + Console.ResetColor(); + } + } + + private bool ExcludeLog(ushort packetId) + { + bool useWhitelist = _packetIdWhitelist.Count > 0; + bool whitelisted = _packetIdWhitelist.Contains(packetId); + bool blacklisted = _packetIdBlacklist.Contains(packetId); + + if (useWhitelist && whitelisted) + { + return false; + } + + if (useWhitelist) + { + return true; + } + + if (blacklisted) + { + return true; + } + + return false; + } + + private string CreatePacketLog(MhfLogPacket logPacket) + { + ushort packetId = logPacket.Id; + + if (ExcludeLog(packetId)) + { + return null; + } + + int dataSize = logPacket.Data.Size; + + StringBuilder sb = new StringBuilder(); + sb.Append($"{logPacket.ClientIdentity} Packet Log"); + sb.Append(Environment.NewLine); + sb.Append("----------"); + sb.Append(Environment.NewLine); + sb.Append($"[{logPacket.TimeStamp:HH:mm:ss}][Typ:{logPacket.LogType}]"); + sb.Append(Environment.NewLine); + sb.Append( + $"[Id:0x{logPacket.Id:X2}|{logPacket.Id}][BodyLen:{logPacket.Data.Size}][{logPacket.PacketIdName}]"); + sb.Append(Environment.NewLine); + sb.Append(logPacket.Header.ToLogText()); + sb.Append(Environment.NewLine); + + if (!NoData) + { + IBuffer data = logPacket.Data; + int maxPacketSize = MaxPacketSize; + if (maxPacketSize > 0 && dataSize > maxPacketSize) + { + data = data.Clone(0, maxPacketSize); + + sb.Append($"- Truncated Data showing {maxPacketSize} of {dataSize} bytes"); + sb.Append(Environment.NewLine); + } + + sb.Append("ASCII:"); + sb.Append(Environment.NewLine); + sb.Append(data.ToAsciiString(true)); + sb.Append(Environment.NewLine); + sb.Append("HEX:"); + sb.Append(Environment.NewLine); + sb.Append(data.ToHexString(' ')); + sb.Append(Environment.NewLine); + } + + sb.Append("----------"); + + return sb.ToString(); + } + } +} diff --git a/Mhf.Cli/Mhf.Cli.csproj b/Mhf.Cli/Mhf.Cli.csproj new file mode 100644 index 0000000..7de7981 --- /dev/null +++ b/Mhf.Cli/Mhf.Cli.csproj @@ -0,0 +1,16 @@ + + + Exe + netcoreapp3.0 + Mhf.Cli + Monster Hunter Frontier Z Command Line Interface + Mhf Team + Mhf.Cli + $(Version) + Copyright © 2019 Mhf Team + 8 + + + + + diff --git a/Mhf.Cli/Program.cs b/Mhf.Cli/Program.cs new file mode 100644 index 0000000..ce58c07 --- /dev/null +++ b/Mhf.Cli/Program.cs @@ -0,0 +1,383 @@ +/* + * This file is part of Mhf.Cli + * + * Mhf.Server is a server implementation for the game "Monster Hunter Frontier Z". + * Copyright (C) 2019-2020 Mhf Team + * + * Github: https://github.com/sebastian-heinz/mhf-server + * + * Mhf.Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mhf.Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Mhf.Server. If not, see . + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Arrowgene.Services.Logging; +using Mhf.Cli.Argument; +using Mhf.Cli.Command; +using Mhf.Cli.Command.Commands; +using Mhf.Server.Common; + +namespace Mhf.Cli +{ + internal class Program + { + public const char CliSeparator = ' '; + public const char CliValueSeparator = '='; + + private static void Main(string[] args) + { + Console.WriteLine("Program started"); + Program program = new Program(); + if (args.Length > 0) + { + program.RunArguments(args); + } + else + { + program.RunInteractive(); + } + + Console.WriteLine("Program ended"); + } + + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly BlockingCollection _inputQueue; + private readonly Thread _consoleThread; + private readonly Dictionary _commands; + private readonly List _parameterConsumers; + private readonly ILogger _logger; + private readonly LogWriter _logWriter; + private readonly SwitchCommand _switchCommand; + + private Program() + { + _logger = LogProvider.Logger(this); + _commands = new Dictionary(); + _inputQueue = new BlockingCollection(); + _parameterConsumers = new List(); + _cancellationTokenSource = new CancellationTokenSource(); + _consoleThread = new Thread(ReadConsoleThread); + _logWriter = new LogWriter(); + _switchCommand = new SwitchCommand(_parameterConsumers); + Console.CancelKeyPress += ConsoleOnCancelKeyPress; + } + + private void LoadCommands() + { + AddCommand(new ShowCommand()); + AddCommand(new ServerCommand(_logWriter)); + AddCommand(new HelpCommand(_commands)); + AddCommand(new ExitCommand()); + AddCommand(new DecryptCommand()); + AddCommand(_switchCommand); + } + + private void LoadGlobalParameterConsumer() + { + _parameterConsumers.Add(_logWriter); + } + + private void RunArguments(string[] arguments) + { + if (arguments.Length <= 0) + { + _logger.Error("Invalid input"); + return; + } + + LoadCommands(); + LoadGlobalParameterConsumer(); + ShowCopyright(); + _logger.Info("Argument Mode"); + _logger.Info("Press `e'-key to exit."); + + ProcessArguments(arguments); + + ConsoleKeyInfo keyInfo = Console.ReadKey(); + while (keyInfo.Key != ConsoleKey.E) + { + keyInfo = Console.ReadKey(); + } + + ShutdownCommands(); + } + + private void RunInteractive() + { + LoadCommands(); + LoadGlobalParameterConsumer(); + ShowCopyright(); + + _logger.Info("Interactive Mode"); + + _consoleThread.IsBackground = true; + _consoleThread.Name = "Console Thread"; + _consoleThread.Start(); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + string line; + try + { + line = _inputQueue.Take(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + line = null; + } + + if (line == null) + { + // Ctrl+Z, Ctrl+C or error + break; + } + + string[] arguments = Util.ParseTextArguments(line, CliSeparator, '"'); + if (arguments.Length <= 0) + { + _logger.Error($"Invalid input: '{line}'. Type 'help' for a list of available commands."); + continue; + } + + CommandResultType result = ProcessArguments(arguments); + if (result == CommandResultType.Exit) + { + break; + } + + if (result == CommandResultType.Continue) + { + continue; + } + + if (result == CommandResultType.Completed) + { + _logger.Info("Command Completed"); + continue; + } + } + + StopReadConsoleThread(); + ShutdownCommands(); + } + + private CommandResultType ProcessArguments(string[] arguments) + { + ConsoleParameter parameter = ParseParameter(arguments); + + if (!_commands.ContainsKey(parameter.Key)) + { + _logger.Error( + $"Command: '{parameter.Key}' not available. Type `help' for a list of available commands."); + return CommandResultType.Continue; + } + + IConsoleCommand consoleCommand = _commands[parameter.Key]; + if (consoleCommand != _switchCommand) + { + _switchCommand.Handle(parameter); + } + + return consoleCommand.Handle(parameter); + } + + /// + /// Parses the input text into arguments and switches. + /// + private ConsoleParameter ParseParameter(string[] args) + { + if (args.Length <= 0) + { + _logger.Error("Invalid input. Type 'help' for a list of available commands."); + return null; + } + + string paramKey = args[0]; + int cmdLength = args.Length - 1; + string[] newArguments = new string[cmdLength]; + if (cmdLength > 0) + { + Array.Copy(args, 1, newArguments, 0, cmdLength); + } + + args = newArguments; + + ConsoleParameter parameter = new ConsoleParameter(paramKey); + foreach (string arg in args) + { + int count = CountChar(arg, CliValueSeparator); + if (count == 1) + { + string[] keyValue = arg.Split(CliValueSeparator); + if (keyValue.Length == 2) + { + string key = keyValue[0]; + string value = keyValue[1]; + if (key.StartsWith('-')) + { + if (key.Length <= 2 || parameter.SwitchMap.ContainsKey(key)) + { + _logger.Error($"Invalid switch key: '{key}' is empty or duplicated."); + continue; + } + + parameter.SwitchMap.Add(key, value); + continue; + } + + if (key.Length <= 0 || parameter.ArgumentMap.ContainsKey(key)) + { + _logger.Error($"Invalid argument key: '{key}' is empty or duplicated."); + continue; + } + + parameter.ArgumentMap.Add(key, value); + continue; + } + } + + if (arg.StartsWith('-')) + { + string switchStr = arg; + if (switchStr.Length <= 2 || parameter.Switches.Contains(switchStr)) + { + _logger.Error($"Invalid switch: '{switchStr}' is empty or duplicated."); + continue; + } + + parameter.Switches.Add(switchStr); + continue; + } + + if (arg.Length <= 0 || parameter.Switches.Contains(arg)) + { + _logger.Error($"Invalid argument: '{arg}' is empty or duplicated."); + continue; + } + + parameter.Arguments.Add(arg); + } + + return parameter; + } + + private int CountChar(string str, char chr) + { + int count = 0; + foreach (char c in str) + { + if (c == chr) + { + count++; + } + } + + return count; + } + + + private void ReadConsoleThread() + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + if (Console.ReadKey().Key != ConsoleKey.Enter) + { + continue; + } + + _logWriter.Pause(); + Console.WriteLine("Enter Command:"); + string line = Console.ReadLine(); + try + { + _inputQueue.Add(line, _cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // Ignored + } + + _logWriter.Continue(); + } + } + + private void StopReadConsoleThread() + { + if (_consoleThread != null + && _consoleThread.IsAlive + && Thread.CurrentThread != _consoleThread + ) + { + try + { + _consoleThread.Interrupt(); + } + catch (Exception) + { + // Ignored + } + + if (!_consoleThread.Join(TimeSpan.FromMilliseconds(500))) + { + try + { + _consoleThread.Abort(); + } + catch (Exception) + { + // Ignored + } + } + } + } + + private void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + _cancellationTokenSource.Cancel(); + } + + private void AddCommand(IConsoleCommand command) + { + _commands.Add(command.Key, command); + } + + private void ShutdownCommands() + { + foreach (IConsoleCommand command in _commands.Values) + { + command.Shutdown(); + } + } + + private void ShowCopyright() + { + StringBuilder sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append("Mhf.Cli Copyright (C) 2019-2020 Mhf Team"); + sb.Append(Environment.NewLine); + sb.Append("This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'."); + sb.Append(Environment.NewLine); + sb.Append("This is free software, and you are welcome to redistribute it"); + sb.Append(Environment.NewLine); + sb.Append("under certain conditions; type `show c' for details."); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + _logger.Info(sb.ToString()); + } + } +} diff --git a/Mhf.Server/Common/BufferProvider.cs b/Mhf.Server/Common/BufferProvider.cs new file mode 100644 index 0000000..b1271b8 --- /dev/null +++ b/Mhf.Server/Common/BufferProvider.cs @@ -0,0 +1,19 @@ +using Arrowgene.Services.Buffers; + +namespace Mhf.Server.Common +{ + public class BufferProvider + { + private static readonly IBufferProvider Provider = new StreamBuffer(); + + public static IBuffer Provide() + { + return Provider.Provide(); + } + + public static IBuffer Provide(byte[] data) + { + return Provider.Provide(data); + } + } +} diff --git a/Mhf.Server/Common/Crc/Crc32.cs b/Mhf.Server/Common/Crc/Crc32.cs new file mode 100644 index 0000000..3a502d2 --- /dev/null +++ b/Mhf.Server/Common/Crc/Crc32.cs @@ -0,0 +1,121 @@ +// Copyright (c) Damien Guard. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; + +namespace Mhf.Server.Common.Crc +{ + /// + /// Implements a 32-bit CRC hash algorithm compatible with Zip etc. + /// + /// + /// Crc32 should only be used for backward compatibility with older file formats + /// and algorithms. It is not secure enough for new applications. + /// If you need to call multiple times for the same data either use the HashAlgorithm + /// interface or remember that the result of one Compute call needs to be ~ (XOR) before + /// being passed in as the seed for the next Compute call. + /// + public sealed class Crc32 : HashAlgorithm + { + public const UInt32 DefaultPolynomial = 0xedb88320u; + public const UInt32 DefaultSeed = 0xffffffffu; + + static UInt32[] defaultTable; + + readonly UInt32 seed; + readonly UInt32[] table; + UInt32 hash; + + public Crc32() + : this(DefaultPolynomial, DefaultSeed) + { + } + + public Crc32(UInt32 polynomial, UInt32 seed) + { + if (!BitConverter.IsLittleEndian) + throw new PlatformNotSupportedException("Not supported on Big Endian processors"); + + table = InitializeTable(polynomial); + this.seed = hash = seed; + } + + public override void Initialize() + { + hash = seed; + } + + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + hash = CalculateHash(table, hash, array, ibStart, cbSize); + } + + protected override byte[] HashFinal() + { + var hashBuffer = UInt32ToBigEndianBytes(~hash); + HashValue = hashBuffer; + return hashBuffer; + } + + public override int HashSize { get { return 32; } } + + public static UInt32 Compute(byte[] buffer) + { + return Compute(DefaultSeed, buffer); + } + + public static UInt32 Compute(UInt32 seed, byte[] buffer) + { + return Compute(DefaultPolynomial, seed, buffer); + } + + public static UInt32 Compute(UInt32 polynomial, UInt32 seed, byte[] buffer) + { + return ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length); + } + + static UInt32[] InitializeTable(UInt32 polynomial) + { + if (polynomial == DefaultPolynomial && defaultTable != null) + return defaultTable; + + var createTable = new UInt32[256]; + for (var i = 0; i < 256; i++) + { + var entry = (UInt32)i; + for (var j = 0; j < 8; j++) + if ((entry & 1) == 1) + entry = (entry >> 1) ^ polynomial; + else + entry = entry >> 1; + createTable[i] = entry; + } + + if (polynomial == DefaultPolynomial) + defaultTable = createTable; + + return createTable; + } + + static UInt32 CalculateHash(UInt32[] table, UInt32 seed, IList buffer, int start, int size) + { + var hash = seed; + for (var i = start; i < start + size; i++) + hash = (hash >> 8) ^ table[buffer[i] ^ hash & 0xff]; + return hash; + } + + static byte[] UInt32ToBigEndianBytes(UInt32 uint32) + { + var result = BitConverter.GetBytes(uint32); + + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + } +} diff --git a/Mhf.Server/Common/CryptoRandom.cs b/Mhf.Server/Common/CryptoRandom.cs new file mode 100644 index 0000000..342a944 --- /dev/null +++ b/Mhf.Server/Common/CryptoRandom.cs @@ -0,0 +1,260 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +namespace Mhf.Server.Common +{ +/* + * Original version by Stephen Toub and Shawn Farkas. + * Random pool and thread safety added by Markus Olsson (freakcode.com). + * + * Original source: http://msdn.microsoft.com/en-us/magazine/cc163367.aspx + * + * Some benchmarks (2009-03-18): + * + * Results produced by calling Next() 1 000 000 times on my machine (dual core 3Ghz) + * + * System.Random completed in 20.4993 ms (avg 0 ms) (first: 0.3454 ms) + * CryptoRandom with pool completed in 132.2408 ms (avg 0.0001 ms) (first: 0.025 ms) + * CryptoRandom without pool completed in 2 sec 587.708 ms (avg 0.0025 ms) (first: 1.4142 ms) + * + * |---------------------|------------------------------------| + * | Implementation | Slowdown compared to System.Random | + * |---------------------|------------------------------------| + * | System.Random | 0 | + * | CryptoRand w pool | 6,6x | + * | CryptoRand w/o pool | 19,5x | + * |---------------------|------------------------------------| + * + * ent (http://www.fourmilab.ch/) results for 16mb of data produced by this class: + * + * > Entropy = 7.999989 bits per byte. + * > + * > Optimum compression would reduce the size of this 16777216 byte file by 0 percent. + * > + * > Chi square distribution for 16777216 samples is 260.64, + * > and randomly would exceed this value 50.00 percent of the times. + * > + * > Arithmetic mean value of data bytes is 127.4974 (127.5 = random). + * > Monte Carlo value for Pi is 3.141838823 (error 0.01 percent). + * > Serial correlation coefficient is 0.000348 (totally uncorrelated = 0.0). + * + * your mileage may vary ;) + * + */ + + /// + /// A random number generator based on the RNGCryptoServiceProvider. + /// Adapted from the "Tales from the CryptoRandom" article in MSDN Magazine (September 2007) + /// but with explicit guarantee to be thread safe. Note that this implementation also includes + /// an optional (enabled by default) random buffer which provides a significant speed boost as + /// it greatly reduces the amount of calls into unmanaged land. + /// + public class CryptoRandom : Random + { + private RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + + private byte[] _buffer; + + private int _bufferPosition; + + /// + /// Gets a value indicating whether this instance has random pool enabled. + /// + /// + /// true if this instance has random pool enabled; otherwise, false. + /// + public bool IsRandomPoolEnabled { get; private set; } + + /// + /// Initializes a new instance of the class with. + /// Using this overload will enable the random buffer pool. + /// + public CryptoRandom() : this(true) + { + } + + /// + /// Initializes a new instance of the class. + /// This method will disregard whatever value is passed as seed and it's only implemented + /// in order to be fully backwards compatible with . + /// Using this overload will enable the random buffer pool. + /// + /// The ignored seed. + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "ignoredSeed", + Justification = "Cannot remove this parameter as we implement the full API of System.Random")] + public CryptoRandom(int ignoredSeed) : this(true) + { + } + + /// + /// Initializes a new instance of the class with + /// optional random buffer. + /// + /// set to true to enable the random pool buffer for increased performance. + public CryptoRandom(bool enableRandomPool) + { + IsRandomPoolEnabled = enableRandomPool; + } + + private void InitBuffer() + { + if (IsRandomPoolEnabled) + { + if (_buffer == null || _buffer.Length != 512) + _buffer = new byte[512]; + } + else + { + if (_buffer == null || _buffer.Length != 4) + _buffer = new byte[4]; + } + + _rng.GetBytes(_buffer); + _bufferPosition = 0; + } + + /// + /// Returns a nonnegative random number. + /// + /// + /// A 32-bit signed integer greater than or equal to zero and less than . + /// + public override int Next() + { + // Mask away the sign bit so that we always return nonnegative integers + return (int) GetRandomUInt32() & 0x7FFFFFFF; + } + + /// + /// Returns a nonnegative random number less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. must be greater than or equal to zero. + /// + /// A 32-bit signed integer greater than or equal to zero, and less than ; that is, the range of return values ordinarily includes zero but not . However, if equals zero, is returned. + /// + /// + /// is less than zero. + /// + public override int Next(int maxValue) + { + if (maxValue < 0) + throw new ArgumentOutOfRangeException("maxValue"); + + return Next(0, maxValue); + } + + /// + /// Returns a random number within a specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. must be greater than or equal to . + /// + /// A 32-bit signed integer greater than or equal to and less than ; that is, the range of return values includes but not . If equals , is returned. + /// + /// + /// is greater than . + /// + public override int Next(int minValue, int maxValue) + { + if (minValue > maxValue) + throw new ArgumentOutOfRangeException("minValue"); + + if (minValue == maxValue) + return minValue; + + long diff = maxValue - minValue; + + while (true) + { + uint rand = GetRandomUInt32(); + + long max = 1 + (long) uint.MaxValue; + long remainder = max % diff; + + if (rand < max - remainder) + return (int) (minValue + (rand % diff)); + } + } + + /// + /// Returns a random number between 0.0 and 1.0. + /// + /// + /// A double-precision floating point number greater than or equal to 0.0, and less than 1.0. + /// + public override double NextDouble() + { + return GetRandomUInt32() / (1.0 + uint.MaxValue); + } + + /// + /// Fills the elements of a specified array of bytes with random numbers. + /// + /// An array of bytes to contain random numbers. + /// + /// is null. + /// + public override void NextBytes(byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + + lock (this) + { + if (IsRandomPoolEnabled && _buffer == null) + InitBuffer(); + + // Can we fit the requested number of bytes in the buffer? + if (IsRandomPoolEnabled && _buffer.Length <= buffer.Length) + { + int count = buffer.Length; + + EnsureRandomBuffer(count); + + Buffer.BlockCopy(_buffer, _bufferPosition, buffer, 0, count); + + _bufferPosition += count; + } + else + { + // Draw bytes directly from the RNGCryptoProvider + _rng.GetBytes(buffer); + } + } + } + + /// + /// Gets one random unsigned 32bit integer in a thread safe manner. + /// + private uint GetRandomUInt32() + { + lock (this) + { + EnsureRandomBuffer(4); + + uint rand = BitConverter.ToUInt32(_buffer, _bufferPosition); + + _bufferPosition += 4; + + return rand; + } + } + + /// + /// Ensures that we have enough bytes in the random buffer. + /// + /// The number of required bytes. + private void EnsureRandomBuffer(int requiredBytes) + { + if (_buffer == null) + InitBuffer(); + + if (requiredBytes > _buffer.Length) + throw new ArgumentOutOfRangeException("requiredBytes", "cannot be greater than random buffer"); + + if ((_buffer.Length - _bufferPosition) < requiredBytes) + InitBuffer(); + } + } +} diff --git a/Mhf.Server/Common/Instance/IInstance.cs b/Mhf.Server/Common/Instance/IInstance.cs new file mode 100644 index 0000000..e2fed41 --- /dev/null +++ b/Mhf.Server/Common/Instance/IInstance.cs @@ -0,0 +1,7 @@ +namespace Mhf.Server.Common.Instance +{ + public interface IInstance + { + uint InstanceId { get; set; } + } +} diff --git a/Mhf.Server/Common/Instance/InstanceGenerator.cs b/Mhf.Server/Common/Instance/InstanceGenerator.cs new file mode 100644 index 0000000..fe25742 --- /dev/null +++ b/Mhf.Server/Common/Instance/InstanceGenerator.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace Mhf.Server.Common.Instance +{ + /// + /// Provides Unique Ids for instancing. + /// + public class InstanceGenerator + { + private readonly object _lock; + private uint _currentId; + + private readonly Dictionary _instances; + + public InstanceGenerator() + { + _lock = new object(); + _currentId = 0; + _instances = new Dictionary(); + } + + public void AssignInstance(IInstance instance) + { + uint id; + lock (_lock) + { + id = _currentId; + _currentId++; + } + + _instances.Add(id, instance); + instance.InstanceId = id; + } + + public T CreateInstance() where T : IInstance, new() + { + uint id; + lock (_lock) + { + id = _currentId; + _currentId++; + } + + T instance = new T(); + _instances.Add(id, instance); + instance.InstanceId = id; + return instance; + } + + public IInstance GetInstance(uint id) + { + if (!_instances.ContainsKey(id)) + { + return null; + } + + return _instances[id]; + } + } +} diff --git a/Mhf.Server/Common/MhfUpDat.cs b/Mhf.Server/Common/MhfUpDat.cs new file mode 100644 index 0000000..90f49c5 --- /dev/null +++ b/Mhf.Server/Common/MhfUpDat.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Text; +using Mhf.Server.Common.Crc; + +namespace Mhf.Server.Common +{ + /// + /// Generates MHFUP.DAT file of all files from specified root directory. + /// The file has entry ended by a LineFeed (LF / 0x0A) + /// + public class MhfUpDat + { + private readonly DirectoryInfo _rootDirectoryInfo; + + public MhfUpDat(string rootDirectoryPath) + { + _rootDirectoryInfo = new DirectoryInfo(rootDirectoryPath); + if (!_rootDirectoryInfo.Exists) + { + throw new FileNotFoundException("Folder does not exist", rootDirectoryPath); + } + } + + public string GetUpdateEntry(string filePath) + { + return GetUpdateEntry(new FileInfo(filePath)); + } + + public string GetUpdateEntry(FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + return null; + } + + fileInfo.Refresh(); + byte[] file = Util.ReadFile(fileInfo.FullName); + uint crc32 = Crc32.Compute(file); + DateTime lastWriteTime = fileInfo.LastWriteTime; + long lastWriteFileTime = lastWriteTime.ToFileTime(); + string lastWriteFileTimeHex = $"{lastWriteFileTime:X16}"; + string fileTimeHex1 = lastWriteFileTimeHex.Substring(8); + string fileTimeHex2 = lastWriteFileTimeHex.Substring(0, 8); + return $"{crc32:X8},{fileTimeHex1},{fileTimeHex2},{GetFileNameEntry(fileInfo)},{file.Length},0"; + } + + public string CreateMhfUpDat() + { + StringBuilder sb = new StringBuilder(); + foreach (FileInfo fileInfo in _rootDirectoryInfo.GetFiles("*", SearchOption.AllDirectories)) + { + sb.Append($"{GetUpdateEntry(fileInfo)}/n"); + } + + return sb.ToString(); + } + + public void SaveMhfUpDat(string destinationFilePath) + { + string mhfUpDat = CreateMhfUpDat(); + Util.WriteFile(Encoding.UTF8.GetBytes(mhfUpDat), destinationFilePath); + } + + private string GetFileNameEntry(FileInfo fileInfo) + { + string relativeDirectory = Util.RelativeDirectory(_rootDirectoryInfo.FullName, fileInfo.DirectoryName); + if (string.IsNullOrEmpty(relativeDirectory)) + { + return $"exe\\{fileInfo.Name}"; + } + + string path = Path.Combine(relativeDirectory, fileInfo.Name); + return path; + } + } +} diff --git a/Mhf.Server/Common/Middleware/IMiddleware.cs b/Mhf.Server/Common/Middleware/IMiddleware.cs new file mode 100644 index 0000000..8d0b13d --- /dev/null +++ b/Mhf.Server/Common/Middleware/IMiddleware.cs @@ -0,0 +1,7 @@ +namespace Mhf.Server.Common.Middleware +{ + public interface IMiddleware + { + void Handle(T user, TReq request, TRes response, MiddlewareDelegate next); + } +} diff --git a/Mhf.Server/Common/Middleware/Middleware.cs b/Mhf.Server/Common/Middleware/Middleware.cs new file mode 100644 index 0000000..8f66cdf --- /dev/null +++ b/Mhf.Server/Common/Middleware/Middleware.cs @@ -0,0 +1,16 @@ +using Arrowgene.Services.Logging; + +namespace Mhf.Server.Common.Middleware +{ + public abstract class Middleware : IMiddleware + { + protected Middleware() + { + Logger = LogProvider.Logger(this); + } + + protected ILogger Logger { get; } + + public abstract void Handle(T client, TReq message, TRes response, MiddlewareDelegate next); + } +} diff --git a/Mhf.Server/Common/Middleware/MiddlewareDelegate.cs b/Mhf.Server/Common/Middleware/MiddlewareDelegate.cs new file mode 100644 index 0000000..036876a --- /dev/null +++ b/Mhf.Server/Common/Middleware/MiddlewareDelegate.cs @@ -0,0 +1,4 @@ +namespace Mhf.Server.Common.Middleware +{ + public delegate void MiddlewareDelegate(T user, TReq request, TRes response); +} diff --git a/Mhf.Server/Common/Middleware/MiddlewareStack.cs b/Mhf.Server/Common/Middleware/MiddlewareStack.cs new file mode 100644 index 0000000..70552af --- /dev/null +++ b/Mhf.Server/Common/Middleware/MiddlewareStack.cs @@ -0,0 +1,34 @@ +using System; + +namespace Mhf.Server.Common.Middleware +{ + /// + /// Implementation of a middleware + /// + public class MiddlewareStack + { + private MiddlewareDelegate _middlewareDelegate; + + public MiddlewareStack(MiddlewareDelegate kernel) + { + _middlewareDelegate = kernel; + } + + public void Start(T user, TReq request, TRes response) + { + _middlewareDelegate(user, request, response); + } + + public MiddlewareStack Use( + Func, MiddlewareDelegate> middleware) + { + _middlewareDelegate = middleware(_middlewareDelegate); + return this; + } + + public MiddlewareStack Use(IMiddleware middleware) + { + return Use(next => (user, request, response) => middleware.Handle(user, request, response, next)); + } + } +} diff --git a/Mhf.Server/Common/Util.cs b/Mhf.Server/Common/Util.cs new file mode 100644 index 0000000..653ba77 --- /dev/null +++ b/Mhf.Server/Common/Util.cs @@ -0,0 +1,695 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Mhf.Server.Common +{ + public class Util + { + public static readonly CryptoRandom Random = new CryptoRandom(); + + private static readonly Random RandomNum = new Random(); + + public static int GetRandomNumber(int min, int max) + { + lock (RandomNum) + { + return RandomNum.Next(min, max); + } + } + + public static long GetUnixTime(DateTime dateTime) + { + return ((DateTimeOffset) dateTime).ToUnixTimeSeconds(); + } + + public static string PathDifferenceEnd(string directoryInfo1, string directoryInfo2, bool unRoot) + { + return PathDifference(new DirectoryInfo(directoryInfo1), new DirectoryInfo(directoryInfo2), unRoot); + } + + public static string PathDifferenceEnd(FileSystemInfo directoryInfo1, FileSystemInfo directoryInfo2, + bool unRoot) + { + string result; + if (directoryInfo1.FullName == directoryInfo2.FullName) + { + result = directoryInfo1.FullName; + } + else if (directoryInfo1.FullName.EndsWith(directoryInfo2.FullName)) + { + result = directoryInfo1.FullName.Split(new[] {directoryInfo2.FullName}, + StringSplitOptions.RemoveEmptyEntries)[0]; + } + else if (directoryInfo2.FullName.EndsWith(directoryInfo1.FullName)) + { + result = directoryInfo2.FullName.Split(new[] {directoryInfo1.FullName}, + StringSplitOptions.RemoveEmptyEntries)[0]; + } + else + { + result = ""; + } + + if (unRoot) + { + result = UnRootPath(result); + } + + return result; + } + + + public static string PathDifference(string directoryInfo1, string directoryInfo2, bool unRoot) + { + return PathDifference(new DirectoryInfo(directoryInfo1), new DirectoryInfo(directoryInfo2), unRoot); + } + + public static string PathDifference(FileSystemInfo directoryInfo1, FileSystemInfo directoryInfo2, bool unRoot) + { + string result; + if (directoryInfo1.FullName == directoryInfo2.FullName) + { + result = ""; + } + else if (directoryInfo1.FullName.StartsWith(directoryInfo2.FullName)) + { + result = directoryInfo1.FullName.Split(new[] {directoryInfo2.FullName}, + StringSplitOptions.RemoveEmptyEntries)[0]; + } + else if (directoryInfo2.FullName.StartsWith(directoryInfo1.FullName)) + { + result = directoryInfo2.FullName.Split(new[] {directoryInfo1.FullName}, + StringSplitOptions.RemoveEmptyEntries)[0]; + } + else + { + result = ""; + } + + if (unRoot) + { + result = UnRootPath(result); + } + + return result; + } + + public static string UnRootPath(string path) + { + // https://stackoverflow.com/questions/53102/why-does-path-combine-not-properly-concatenate-filenames-that-start-with-path-di + if (Path.IsPathRooted(path)) + { + path = path.TrimStart(Path.DirectorySeparatorChar); + path = path.TrimStart(Path.AltDirectorySeparatorChar); + } + + return path; + } + + public static AssemblyName GetAssemblyName(string name) + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + AssemblyName assemblyName = assembly.GetName(); + if (assemblyName.Name == name) + { + return assemblyName; + } + } + + return null; + } + + public static Version GetAssemblyVersion(string name) + { + AssemblyName assemblyName = GetAssemblyName(name); + if (assemblyName != null) + { + return assemblyName.Version; + } + + return null; + } + + public static string GetAssemblyVersionString(string name) + { + Version version = GetAssemblyVersion(name); + if (version != null) + { + return version.ToString(); + } + + return null; + } + + public static byte[] ReadFile(string source) + { + if (!File.Exists(source)) + { + throw new Exception(string.Format("'{0}' does not exist or is not a file", source)); + } + + return File.ReadAllBytes(source); + } + + public static string ReadFileText(string source) + { + if (!File.Exists(source)) + { + throw new Exception($"'{source}' does not exist or is not a file"); + } + + return File.ReadAllText(source); + } + + public static void WriteFile(byte[] content, string destination) + { + if (content != null) + { + File.WriteAllBytes(destination, content); + } + else + { + throw new Exception($"Content of '{destination}' is null"); + } + } + + public static List GetFiles(DirectoryInfo directoryInfo, string[] extensions, bool recursive) + { + if (recursive) + { + List filteredFiles = GetFiles(directoryInfo, extensions); + DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories("*", SearchOption.TopDirectoryOnly); + foreach (DirectoryInfo dInfo in directoryInfos) + { + List files = GetFiles(dInfo, extensions, true); + filteredFiles.AddRange(files); + } + + return filteredFiles; + } + + return GetFiles(directoryInfo, extensions); + } + + public static List GetFiles(DirectoryInfo directoryInfo, string[] extensions) + { + List filteredFiles = new List(); + FileInfo[] files = directoryInfo.GetFiles("*.*", SearchOption.TopDirectoryOnly); + foreach (FileInfo file in files) + { + if (extensions != null) + { + foreach (string extension in extensions) + { + if (file.Extension.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) + { + filteredFiles.Add(file); + break; + } + } + } + else + { + filteredFiles.Add(file); + } + } + + return filteredFiles; + } + + public static List GetFolders(DirectoryInfo directoryInfo, string[] extensions, bool recursive) + { + if (recursive) + { + List result = new List(); + List filteredDirectories = GetFolders(directoryInfo, extensions); + result.AddRange(filteredDirectories); + foreach (DirectoryInfo directory in filteredDirectories) + { + List directories = GetFolders(directory, extensions, true); + result.AddRange(directories); + } + + return result; + } + + return GetFolders(directoryInfo, extensions); + } + + public static List GetFolders(DirectoryInfo directoryInfo, string[] extensions) + { + List filteredDirectories = new List(); + DirectoryInfo[] directories = directoryInfo.GetDirectories("*", SearchOption.TopDirectoryOnly); + foreach (DirectoryInfo directory in directories) + { + if (extensions != null) + { + foreach (string extension in extensions) + { + if (directory.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) + { + filteredDirectories.Add(directory); + break; + } + } + } + else + { + filteredDirectories.Add(directory); + } + } + + return filteredDirectories; + } + + + public static DirectoryInfo EnsureDirectory(string directory) + { + return Directory.CreateDirectory(directory); + } + + /// + /// The directory of the executing assembly. + /// This might not be the location where the .dll files are located. + /// + /// + public static string ExecutingDirectory() + { + string path = Assembly.GetEntryAssembly().CodeBase; + Uri uri = new Uri(path); + string directory = Path.GetDirectoryName(uri.LocalPath); + return directory; + } + + /// + /// The relative directory of the executing assembly. + /// This might not be the location where the .dll files are located. + /// + public static string RelativeExecutingDirectory() + { + return RelativeDirectory(Environment.CurrentDirectory, ExecutingDirectory()); + } + + /// + /// Directory of Common.dll + /// This is expected to contain ressource files. + /// + public static string CommonDirectory() + { + string location = typeof(Util).GetTypeInfo().Assembly.Location; + Uri uri = new Uri(location); + string directory = Path.GetDirectoryName(uri.LocalPath); + return directory; + } + + /// + /// Relative Directory of Common.dll. + /// This is expected to contain ressource files. + /// + public static string RelativeCommonDirectory() + { + return RelativeDirectory(Environment.CurrentDirectory, CommonDirectory()); + } + + public static string CreateMd5(string input) + { + MD5 md5 = MD5.Create(); + byte[] inputBytes = Encoding.ASCII.GetBytes(input); + byte[] hashBytes = md5.ComputeHash(inputBytes); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes[i].ToString("X2")); + } + + return sb.ToString().ToLower(); + } + + public static string RelativeDirectory(string fromDirectory, string toDirectory) + { + return RelativeDirectory(fromDirectory, toDirectory, toDirectory, Path.DirectorySeparatorChar); + } + + public static string RelativeDirectory(string fromDirectory, string toDirectory, string defaultDirectory) + { + return RelativeDirectory(fromDirectory, toDirectory, defaultDirectory, Path.DirectorySeparatorChar); + } + + /// + /// Returns a directory that is relative. + /// + /// The directory to navigate from. + /// The directory to reach. + /// A directory to return on failure. + /// + /// The relative directory or the defaultDirectory on failure. + public static string RelativeDirectory(string fromDirectory, string toDirectory, string defaultDirectory, + char directorySeparator) + { + string result; + + if (fromDirectory.EndsWith("\\") || fromDirectory.EndsWith("/")) + { + fromDirectory = fromDirectory.Remove(fromDirectory.Length - 1); + } + + if (toDirectory.EndsWith("\\") || toDirectory.EndsWith("/")) + { + toDirectory = toDirectory.Remove(toDirectory.Length - 1); + } + + if (toDirectory.StartsWith(fromDirectory)) + { + result = toDirectory.Substring(fromDirectory.Length); + if (result.StartsWith("\\") || result.StartsWith("/")) + { + result = result.Substring(1, result.Length - 1); + } + + if (result != "") + { + result += directorySeparator; + } + } + else + { + string[] fromDirs = fromDirectory.Split(':', '\\', '/'); + string[] toDirs = toDirectory.Split(':', '\\', '/'); + if (fromDirs.Length <= 0 || toDirs.Length <= 0 || fromDirs[0] != toDirs[0]) + { + return defaultDirectory; + } + + int offset = 1; + for (; offset < fromDirs.Length; offset++) + { + if (toDirs.Length <= offset) + { + break; + } + + if (fromDirs[offset] != toDirs[offset]) + { + break; + } + } + + StringBuilder relativeBuilder = new StringBuilder(); + for (int i = 0; i < fromDirs.Length - offset; i++) + { + relativeBuilder.Append(".."); + relativeBuilder.Append(directorySeparator); + } + + for (int i = offset; i < toDirs.Length - 1; i++) + { + relativeBuilder.Append(toDirs[i]); + relativeBuilder.Append(directorySeparator); + } + + result = relativeBuilder.ToString(); + } + + result = DirectorySeparator(result, directorySeparator); + return result; + } + + public static string DirectorySeparator(string path) + { + return DirectorySeparator(path, Path.DirectorySeparatorChar); + } + + public static string DirectorySeparator(string path, char directorySeparator) + { + if (directorySeparator != '\\') + { + path = path.Replace('\\', directorySeparator); + } + + if (directorySeparator != '/') + { + path = path.Replace('/', directorySeparator); + } + + return path; + } + + public static string GenerateSessionKey(int desiredLength) + { + StringBuilder sessionKey = new StringBuilder(); + using (RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider()) + { + byte[] random = new byte[1]; + int length = 0; + while (length < desiredLength) + { + cryptoProvider.GetBytes(random); + char c = (char) random[0]; + if ((Char.IsDigit(c) || Char.IsLetter(c)) && random[0] < 127) + { + length++; + sessionKey.Append(c); + } + } + } + + return sessionKey.ToString(); + } + + public static byte[] GenerateKey(int desiredLength) + { + byte[] random = new byte[desiredLength]; + using (RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider()) + { + cryptoProvider.GetNonZeroBytes(random); + } + + return random; + } + + /// + /// Removes entries from a collection. + /// The input lists are not modified, instead a new collection is returned. + /// + public static TList SubtractList(TList entries, params TItem[] excepts) + where TList : ICollection, new() + { + TList result = new TList(); + foreach (TItem entry in entries) + { + result.Add(entry); + } + + foreach (TItem except in excepts) + { + result.Remove(except); + } + + return result; + } + + public static byte[] FromHexString(string hexString) + { + if ((hexString.Length & 1) != 0) + { + throw new ArgumentException("Input must have even number of characters"); + } + byte[] ret = new byte[hexString.Length/2]; + for (int i = 0; i < ret.Length; i++) + { + int high = hexString[i*2]; + int low = hexString[i*2+1]; + high = (high & 0xf) + ((high & 0x40) >> 6) * 9; + low = (low & 0xf) + ((low & 0x40) >> 6) * 9; + + ret[i] = (byte)((high << 4) | low); + } + + return ret; + } + + public static string ToHexString(byte[] data, char? seperator = null) + { + StringBuilder sb = new StringBuilder(); + int len = data.Length; + for (int i = 0; i < len; i++) + { + sb.Append(data[i].ToString("X2")); + if (seperator != null && i < len - 1) + { + sb.Append(seperator); + } + } + + return sb.ToString(); + } + + public static string ToAsciiString(byte[] data, bool spaced) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < data.Length; i++) + { + char c = '.'; + if (data[i] >= 'A' && data[i] <= 'Z') c = (char) data[i]; + if (data[i] >= 'a' && data[i] <= 'z') c = (char) data[i]; + if (data[i] >= '0' && data[i] <= '9') c = (char) data[i]; + if (spaced && i != 0) + { + sb.Append(" "); + } + + sb.Append(c); + } + + return sb.ToString(); + } + + public static string[] ParseTextArguments(string line, char delimiter, char textQualifier) + { + IList list = ParseTextList(line, delimiter, textQualifier); + int count = list.Count; + string[] arguments = new string[count]; + for (int i = 0; i < count; i++) + { + arguments[i] = list[i]; + } + + return arguments; + } + + public static IEnumerable ParseTextEnumerable(string line, char delimiter, char textQualifier) + { + if (string.IsNullOrWhiteSpace(line)) + yield break; + + char prevChar = '\0'; + char nextChar = '\0'; + char currentChar = '\0'; + bool inString = false; + + StringBuilder token = new StringBuilder(); + + for (int i = 0; i < line.Length; i++) + { + currentChar = line[i]; + + if (i > 0) + prevChar = line[i - 1]; + else + prevChar = '\0'; + + if (i + 1 < line.Length) + nextChar = line[i + 1]; + else + nextChar = '\0'; + + if (currentChar == textQualifier && (prevChar == '\0' || prevChar == delimiter) && !inString) + { + inString = true; + continue; + } + + if (currentChar == textQualifier && (nextChar == '\0' || nextChar == delimiter) && inString) + { + inString = false; + continue; + } + + if (currentChar == delimiter && !inString) + { + yield return token.ToString(); + token = token.Remove(0, token.Length); + continue; + } + + token = token.Append(currentChar); + yield return token.ToString(); + } + } + + public static IList ParseTextList(string line, char delimiter, char textQualifier) + { + IList collection = new List(); + if (string.IsNullOrWhiteSpace(line)) + return collection; + + char prevChar = '\0'; + char nextChar = '\0'; + char currentChar = '\0'; + bool inString = false; + + StringBuilder token = new StringBuilder(); + + for (int i = 0; i < line.Length; i++) + { + currentChar = line[i]; + + if (i > 0) + prevChar = line[i - 1]; + else + prevChar = '\0'; + + if (i + 1 < line.Length) + nextChar = line[i + 1]; + else + nextChar = '\0'; + + if (currentChar == textQualifier && (prevChar == '\0' || prevChar == delimiter) && !inString) + { + inString = true; + continue; + } + + if (currentChar == textQualifier && (nextChar == '\0' || nextChar == delimiter) && inString) + { + inString = false; + continue; + } + + if (currentChar == delimiter && !inString) + { + collection.Add(token.ToString()); + token = token.Remove(0, token.Length); + continue; + } + + token = token.Append(currentChar); + } + + collection.Add(token.ToString()); + return collection; + } + + /// + /// Read a stream till the end and return the read bytes. + /// + public static async Task ReadAsync(Stream stream) + { + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + byte[] result = new byte[0]; + int offset = 0; + int read = 0; + while ((read = await stream.ReadAsync(buffer, 0, bufferSize)) > 0) + { + int newSize = offset + read; + byte[] temp = new byte[newSize]; + Buffer.BlockCopy(result, 0, temp, 0, offset); + Buffer.BlockCopy(buffer, 0, temp, offset, read); + result = temp; + offset += read; + } + + return result; + } + + } +} diff --git a/Mhf.Server/Database/IDatabase.cs b/Mhf.Server/Database/IDatabase.cs new file mode 100644 index 0000000..1b0993a --- /dev/null +++ b/Mhf.Server/Database/IDatabase.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Mhf.Server.Model; + +namespace Mhf.Server.Database +{ + public interface IDatabase + { + void Execute(string sql); + + /// + /// Return true if database was created, or false if not. + /// + bool CreateDatabase(); + + // Account + Account CreateAccount(string name, string mail, string hash); + Account SelectAccountById(int accountId); + Account SelectAccountByName(string accountName); + bool UpdateAccount(Account account); + bool DeleteAccount(int accountId); + } +} diff --git a/Mhf.Server/Database/MhfDatabaseBuilder.cs b/Mhf.Server/Database/MhfDatabaseBuilder.cs new file mode 100644 index 0000000..a121ecd --- /dev/null +++ b/Mhf.Server/Database/MhfDatabaseBuilder.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using Arrowgene.Services.Logging; +using Mhf.Server.Database.Sql; +using Mhf.Server.Model; +using Mhf.Server.Setting; + +namespace Mhf.Server.Database +{ + public class MhfDatabaseBuilder + { + private readonly ILogger _logger; + + public MhfDatabaseBuilder() + { + _logger = LogProvider.Logger(this); + } + + public IDatabase Build(DatabaseSetting settings) + { + IDatabase database = null; + switch (settings.Type) + { + case DatabaseType.SQLite: + database = PrepareSqlLiteDb(settings.SqLiteFolder); + break; + } + + if (database == null) + { + _logger.Error("Database could not be created, exiting..."); + Environment.Exit(1); + } + + return database; + } + + private MhfSqLiteDb PrepareSqlLiteDb(string sqLiteFolder) + { + string sqLitePath = Path.Combine(sqLiteFolder, $"db.v{MhfSqLiteDb.Version}.sqlite"); + MhfSqLiteDb db = new MhfSqLiteDb(sqLitePath); + if (db.CreateDatabase()) + { + ScriptRunner scriptRunner = new ScriptRunner(db); + scriptRunner.Run(Path.Combine(sqLiteFolder, "Script/schema_sqlite.sql")); + scriptRunner.Run(Path.Combine(sqLiteFolder, "Script/data_account.sql")); + } + + return db; + } + } +} diff --git a/Mhf.Server/Database/Sql/Core/MhfSqlDb.cs b/Mhf.Server/Database/Sql/Core/MhfSqlDb.cs new file mode 100644 index 0000000..e041560 --- /dev/null +++ b/Mhf.Server/Database/Sql/Core/MhfSqlDb.cs @@ -0,0 +1,29 @@ +using System; +using System.Data.Common; +using Arrowgene.Services.Logging; +using Mhf.Server.Logging; + +namespace Mhf.Server.Database.Sql.Core +{ + /// + /// Implementation of Mhf database operations. + /// + public abstract partial class MhfSqlDb : SqlDb + where TCon : DbConnection + where TCom : DbCommand + { + protected readonly MhfLogger Logger; + + + public MhfSqlDb() + { + Logger = LogProvider.Logger(this); + } + + protected override void Exception(Exception ex) + { + Logger.Exception(ex); + } + + } +} diff --git a/Mhf.Server/Database/Sql/Core/MhfSqlDbAccount.cs b/Mhf.Server/Database/Sql/Core/MhfSqlDbAccount.cs new file mode 100644 index 0000000..b50d9ff --- /dev/null +++ b/Mhf.Server/Database/Sql/Core/MhfSqlDbAccount.cs @@ -0,0 +1,131 @@ +using System; +using System.Data.Common; +using Mhf.Server.Model; + +namespace Mhf.Server.Database.Sql.Core +{ + public abstract partial class MhfSqlDb : SqlDb + where TCon : DbConnection + where TCom : DbCommand + { + private const string SqlInsertAccount = + "INSERT INTO `account` (`name`, `normal_name`, `hash`, `mail`, `mail_verified`, `mail_verified_at`, `mail_token`, `password_token`, `state`, `last_login`, `created`) VALUES (@name, @normal_name, @hash, @mail, @mail_verified, @mail_verified_at, @mail_token, @password_token, @state, @last_login, @created);"; + + private const string SqlSelectAccountById = + "SELECT `id`, `name`, `normal_name`, `hash`, `mail`, `mail_verified`, `mail_verified_at`, `mail_token`, `password_token`, `state`, `last_login`, `created` FROM `account` WHERE `id`=@id;"; + + private const string SqlSelectAccountByName = + "SELECT `id`, `name`, `normal_name`, `hash`, `mail`, `mail_verified`, `mail_verified_at`, `mail_token`, `password_token`, `state`, `last_login`, `created` FROM `account` WHERE `name`=@name;"; + + private const string SqlUpdateAccount = + "UPDATE `account` SET `name`=@name, `normal_name`=@normal_name, `hash`=@hash, `mail`=@mail, `mail_verified`=@mail_verified, `mail_verified_at`=@mail_verified_at, `mail_token`=@mail_token, `password_token`=@password_token, `state`=@state, `last_login`=@last_login, `created`=@created WHERE `id`=@id;"; + + private const string SqlDeleteAccount = + "DELETE FROM `account` WHERE `id`=@id;"; + + public Account CreateAccount(string name, string mail, string hash) + { + Account account = new Account(); + account.Name = name; + account.NormalName = name.ToLowerInvariant(); + account.Mail = mail; + account.Hash = hash; + account.State = AccountStateType.User; + account.Created = DateTime.Now; + int rowsAffected = ExecuteNonQuery(SqlInsertAccount, command => + { + AddParameter(command, "@name", account.Name); + AddParameter(command, "@normal_name", account.NormalName); + AddParameter(command, "@hash", account.Hash); + AddParameter(command, "@mail", account.Mail); + AddParameter(command, "@mail_verified", account.MailVerified); + AddParameter(command, "@mail_verified_at", account.MailVerifiedAt); + AddParameter(command, "@mail_token", account.MailToken); + AddParameter(command, "@password_token", account.PasswordToken); + AddParameterEnumInt32(command, "@state", account.State); + AddParameter(command, "@last_login", account.LastLogin); + AddParameter(command, "@created", account.Created); + }, out long autoIncrement); + if (rowsAffected <= NoRowsAffected || autoIncrement <= NoAutoIncrement) + { + return null; + } + + account.Id = (int) autoIncrement; + return account; + } + + public Account SelectAccountByName(string accountName) + { + Account account = null; + ExecuteReader(SqlSelectAccountByName, + command => { AddParameter(command, "@name", accountName); }, reader => + { + if (reader.Read()) + { + account = ReadAccount(reader); + } + }); + + return account; + } + + public Account SelectAccountById(int accountId) + { + Account account = null; + ExecuteReader(SqlSelectAccountById, command => { AddParameter(command, "@id", accountId); }, reader => + { + if (reader.Read()) + { + account = ReadAccount(reader); + } + }); + return account; + } + + public bool UpdateAccount(Account account) + { + int rowsAffected = ExecuteNonQuery(SqlUpdateAccount, command => + { + AddParameter(command, "@name", account.Name); + AddParameter(command, "@normal_name", account.NormalName); + AddParameter(command, "@hash", account.Hash); + AddParameter(command, "@mail", account.Mail); + AddParameter(command, "@mail_verified", account.MailVerified); + AddParameter(command, "@mail_verified_at", account.MailVerifiedAt); + AddParameter(command, "@mail_token", account.MailToken); + AddParameter(command, "@password_token", account.PasswordToken); + AddParameterEnumInt32(command, "@state", account.State); + AddParameter(command, "@last_login", account.LastLogin); + AddParameter(command, "@created", account.Created); + AddParameter(command, "@id", account.Id); + }); + return rowsAffected > NoRowsAffected; + } + + public bool DeleteAccount(int accountId) + { + int rowsAffected = ExecuteNonQuery(SqlDeleteAccount, + command => { AddParameter(command, "@id", accountId); }); + return rowsAffected > NoRowsAffected; + } + + private Account ReadAccount(DbDataReader reader) + { + Account account = new Account(); + account.Id = GetInt32(reader, "id"); + account.Name = GetString(reader, "name"); + account.NormalName = GetString(reader, "normal_name"); + account.Hash = GetString(reader, "hash"); + account.Mail = GetString(reader, "mail"); + account.MailVerified = GetBoolean(reader, "mail_verified"); + account.MailVerifiedAt = GetDateTimeNullable(reader, "mail_verified_at"); + account.MailToken = GetStringNullable(reader, "mail_token"); + account.PasswordToken = GetStringNullable(reader, "password_token"); + account.State = (AccountStateType) GetInt32(reader, "state"); + account.LastLogin = GetDateTimeNullable(reader, "last_login"); + account.Created = GetDateTime(reader, "created"); + return account; + } + } +} diff --git a/Mhf.Server/Database/Sql/MhfMariaDb.cs b/Mhf.Server/Database/Sql/MhfMariaDb.cs new file mode 100644 index 0000000..34c57a9 --- /dev/null +++ b/Mhf.Server/Database/Sql/MhfMariaDb.cs @@ -0,0 +1,55 @@ +using System; +using MySql.Data.MySqlClient; +using Mhf.Server.Database.Sql.Core; + +namespace Mhf.Server.Database.Sql +{ + /// + /// SQLite Mhf database. + /// + public class MhfMariaDb : MhfSqlDb, IDatabase + { + public const string MemoryDatabasePath = ":memory:"; + + private const string SelectAutoIncrement = "SELECT last_insert_rowid()"; + + + private string _connectionString; + + public bool CreateDatabase() + { + throw new NotImplementedException(); + } + + public MhfMariaDb(string host, short port, string user, string password, string database) + { + _connectionString = $"host={host};port={port};user id={user};password={password};database={database};"; + } + + protected override MySqlConnection Connection() + { + MySqlConnection connection = new MySqlConnection(_connectionString); + connection.Open(); + return connection; + } + + protected override MySqlCommand Command(string query, MySqlConnection connection) + { + MySqlCommand command = connection.CreateCommand(); + command.CommandText = query; + return command; + } + + protected override long AutoIncrement(MySqlConnection connection, MySqlCommand command) + { + return command.LastInsertedId; + } + + public override int Upsert(string table, string[] columns, object[] values, string whereColumn, + object whereValue, + out long autoIncrement) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mhf.Server/Database/Sql/MhfSqLiteDb.cs b/Mhf.Server/Database/Sql/MhfSqLiteDb.cs new file mode 100644 index 0000000..167fcbd --- /dev/null +++ b/Mhf.Server/Database/Sql/MhfSqLiteDb.cs @@ -0,0 +1,96 @@ +using System; +using System.Data.SQLite; +using System.IO; +using Mhf.Server.Database.Sql.Core; + +namespace Mhf.Server.Database.Sql +{ + /// + /// SQLite Mhf database. + /// + public class MhfSqLiteDb : MhfSqlDb, IDatabase + { + public const string MemoryDatabasePath = ":memory:"; + public const int Version = 1; + + private const string SelectAutoIncrement = "SELECT last_insert_rowid()"; + + private readonly string _databasePath; + private string _connectionString; + + public MhfSqLiteDb(string databasePath) + { + _databasePath = databasePath; + Logger.Info($"Database Path: {_databasePath}"); + } + + public bool CreateDatabase() + { + if (_databasePath != MemoryDatabasePath && !File.Exists(_databasePath)) + { + FileStream fs = File.Create(_databasePath); + fs.Close(); + fs.Dispose(); + Logger.Info($"Created new v{Version} database"); + return true; + } + + return false; + } + + private string BuildConnectionString(string source) + { + SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder(); + builder.DataSource = source; + builder.Version = 3; + builder.ForeignKeys = true; + // Set ADO.NET conformance flag https://system.data.sqlite.org/index.html/info/e36e05e299 + builder.Flags = builder.Flags & SQLiteConnectionFlags.StrictConformance; + + string connectionString = builder.ToString(); + Logger.Info($"Connection String: {connectionString}"); + return connectionString; + } + + protected override SQLiteConnection Connection() + { + if (_connectionString == null) + { + _connectionString = BuildConnectionString(_databasePath); + } + + SQLiteConnection connection = new SQLiteConnection(_connectionString); + return connection.OpenAndReturn(); + } + + protected override SQLiteCommand Command(string query, SQLiteConnection connection) + { + return new SQLiteCommand(query, connection); + } + + /// + /// Thread Safe on Connection basis. + /// http://www.sqlite.org/c3ref/last_insert_rowid.html + /// + protected override long AutoIncrement(SQLiteConnection connection, SQLiteCommand command) + { + return connection.LastInsertRowId; + // long autoIncrement = NoAutoIncrement; + // ExecuteReader(SelectAutoIncrement, reader => + // { + // if (reader.Read()) + // { + // autoIncrement = reader.GetInt32(0); + // } + // }); + // return autoIncrement; + } + + public override int Upsert(string table, string[] columns, object[] values, string whereColumn, + object whereValue, + out long autoIncrement) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mhf.Server/Database/Sql/ScriptRunner.cs b/Mhf.Server/Database/Sql/ScriptRunner.cs new file mode 100644 index 0000000..096d74d --- /dev/null +++ b/Mhf.Server/Database/Sql/ScriptRunner.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Text; +using Arrowgene.Services.Logging; + +namespace Mhf.Server.Database.Sql +{ + public class ScriptRunner + { + private const string DEFAULT_DELIMITER = ";"; + + private readonly ILogger _logger; + private IDatabase _database; + private string delimiter = DEFAULT_DELIMITER; + private bool fullLineDelimiter = false; + + /** + * Default constructor + */ + public ScriptRunner(IDatabase database) + { + _database = database; + _logger = LogProvider.Logger(this); + } + + public void Run(string path) + { + int index = 0; + try + { + string[] file = File.ReadAllLines(path); + StringBuilder command = null; + for (; index < file.Length; index++) + { + string line = file[index]; + if (command == null) + { + command = new StringBuilder(); + } + + string trimmedLine = line.Trim(); + + if (trimmedLine.Length < 1) + { + // Do nothing + } + else if (trimmedLine.StartsWith("//") || trimmedLine.StartsWith("--")) + { + // Print comment + } + else if (!fullLineDelimiter && trimmedLine.EndsWith(delimiter) + || fullLineDelimiter && trimmedLine == delimiter) + { + command.Append( + line.Substring(0, line.LastIndexOf(delimiter, StringComparison.InvariantCulture))); + command.Append(" "); + _database.Execute(command.ToString()); + command = null; + } + else + { + command.Append(line); + command.Append("\n"); + } + } + + if (command != null) + { + string cmd = command.ToString(); + if (string.IsNullOrWhiteSpace(cmd)) + { + //do nothing; + } + else + { + _database.Execute(cmd); + } + } + } + catch (Exception exception) + { + _logger.Error($"Sql error at Line: {index}"); + _logger.Exception(exception); + } + } + } +} diff --git a/Mhf.Server/Database/Sql/SqlDb.cs b/Mhf.Server/Database/Sql/SqlDb.cs new file mode 100644 index 0000000..c5a8685 --- /dev/null +++ b/Mhf.Server/Database/Sql/SqlDb.cs @@ -0,0 +1,286 @@ +using System; +using System.Data; +using System.Data.Common; + +namespace Mhf.Server.Database.Sql +{ + /// + /// Operations for SQL type databases. + /// + public abstract class SqlDb + where TCon : DbConnection + where TCom : DbCommand + { + public const int NoRowsAffected = 0; + public const long NoAutoIncrement = 0; + + public SqlDb() + { + } + + protected abstract TCon Connection(); + protected abstract TCom Command(string query, TCon connection); + protected abstract long AutoIncrement(TCon connection, TCom command); + + public abstract int Upsert(string table, string[] columns, object[] values, string whereColumn, + object whereValue, out long autoIncrement); + + public int ExecuteNonQuery(string query, Action nonQueryAction) + { + try + { + using (TCon connection = Connection()) + { + using (TCom command = Command(query, connection)) + { + nonQueryAction(command); + int rowsAffected = command.ExecuteNonQuery(); + return rowsAffected; + } + } + } + catch (Exception ex) + { + Exception(ex); + return NoRowsAffected; + } + } + + public int ExecuteNonQuery(string query, Action nonQueryAction, out long autoIncrement) + { + try + { + using (TCon connection = Connection()) + { + using (TCom command = Command(query, connection)) + { + nonQueryAction(command); + int rowsAffected = command.ExecuteNonQuery(); + autoIncrement = AutoIncrement(connection, command); + return rowsAffected; + } + } + } + catch (Exception ex) + { + Exception(ex); + autoIncrement = NoAutoIncrement; + return NoRowsAffected; + } + } + + public void ExecuteReader(string query, Action nonQueryAction, Action readAction) + { + try + { + using (TCon connection = Connection()) + { + using (TCom command = Command(query, connection)) + { + nonQueryAction(command); + using (DbDataReader reader = command.ExecuteReader()) + { + readAction(reader); + } + } + } + } + catch (Exception ex) + { + Exception(ex); + } + } + + public void ExecuteReader(string query, Action readAction) + { + try + { + using (TCon connection = Connection()) + { + using (TCom command = Command(query, connection)) + { + using (DbDataReader reader = command.ExecuteReader()) + { + readAction(reader); + } + } + } + } + catch (Exception ex) + { + Exception(ex); + } + } + + public void Execute(string query) + { + try + { + using (TCon connection = Connection()) + { + using (TCom command = Command(query, connection)) + { + command.ExecuteNonQuery(); + } + } + } + catch (Exception ex) + { + Exception(ex); + } + } + + public string ServerVersion() + { + try + { + using (TCon connection = Connection()) + { + return connection.ServerVersion; + } + } + catch (Exception ex) + { + Exception(ex); + return string.Empty; + } + } + + protected virtual void Exception(Exception ex) + { + throw ex; + } + + protected DbParameter Parameter(TCom command, string name, object value, DbType type) + { + DbParameter parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + parameter.DbType = type; + return parameter; + } + + protected DbParameter Parameter(TCom command, string name, string value) + { + return Parameter(command, name, value, DbType.String); + } + + protected void AddParameter(TCom command, string name, object value, DbType type) + { + DbParameter parameter = Parameter(command, name, value, type); + command.Parameters.Add(parameter); + } + + protected void AddParameter(TCom command, string name, string value) + { + AddParameter(command, name, value, DbType.String); + } + + protected void AddParameter(TCom command, string name, Int32 value) + { + AddParameter(command, name, value, DbType.Int32); + } + + protected void AddParameter(TCom command, string name, float value) + { + AddParameter(command, name, value, DbType.Double); + } + + protected void AddParameter(TCom command, string name, byte value) + { + AddParameter(command, name, value, DbType.Byte); + } + + protected void AddParameter(TCom command, string name, UInt32 value) + { + AddParameter(command, name, value, DbType.UInt32); + } + + protected void AddParameterEnumInt32(TCom command, string name, T value) where T : Enum + { + AddParameter(command, name, (Int32) (object) value, DbType.Int32); + } + + protected void AddParameter(TCom command, string name, DateTime? value) + { + AddParameter(command, name, value, DbType.DateTime); + } + + protected void AddParameter(TCom command, string name, DateTime value) + { + AddParameter(command, name, value, DbType.DateTime); + } + + protected void AddParameter(TCom command, string name, bool value) + { + AddParameter(command, name, value, DbType.Boolean); + } + + protected DateTime? GetDateTimeNullable(DbDataReader reader, int ordinal) + { + if (reader.IsDBNull(ordinal)) + { + return null; + } + + return reader.GetDateTime(ordinal); + } + + protected string GetStringNullable(DbDataReader reader, int ordinal) + { + if (reader.IsDBNull(ordinal)) + { + return null; + } + + return reader.GetString(ordinal); + } + + protected int GetInt32(DbDataReader reader, string column) + { + return reader.GetInt32(reader.GetOrdinal(column)); + } + + protected byte GetByte(DbDataReader reader, string column) + { + return reader.GetByte(reader.GetOrdinal(column)); + } + + protected short GetInt16(DbDataReader reader, string column) + { + return reader.GetInt16(reader.GetOrdinal(column)); + } + + protected float GetFloat(DbDataReader reader, string column) + { + return reader.GetFloat(reader.GetOrdinal(column)); + } + + protected string GetString(DbDataReader reader, string column) + { + return reader.GetString(reader.GetOrdinal(column)); + } + + protected bool GetBoolean(DbDataReader reader, string column) + { + return reader.GetBoolean(reader.GetOrdinal(column)); + } + + protected DateTime GetDateTime(DbDataReader reader, string column) + { + return reader.GetDateTime(reader.GetOrdinal(column)); + } + + protected DateTime? GetDateTimeNullable(DbDataReader reader, string column) + { + int ordinal = reader.GetOrdinal(column); + return GetDateTimeNullable(reader, ordinal); + } + + protected string GetStringNullable(DbDataReader reader, string column) + { + int ordinal = reader.GetOrdinal(column); + return GetStringNullable(reader, ordinal); + } + } +} diff --git a/Mhf.Server/Files/Database/Script/data_account.sql b/Mhf.Server/Files/Database/Script/data_account.sql new file mode 100644 index 0000000..0f2aa36 --- /dev/null +++ b/Mhf.Server/Files/Database/Script/data_account.sql @@ -0,0 +1,2 @@ +INSERT INTO `account` (id,name,normal_name,hash,mail,mail_verified,mail_verified_at,mail_token,password_token,state,last_login,created) VALUES +(1,'admin','admin','$2a$10$q38QrceMiKLxI3rj6zGpHOQTqG61pEbu5wExo6bhV3s5srxbg11Oi','admin',0,NULL,NULL,NULL,1,NULL,'2019-11-09 20:23:33.2347542'); \ No newline at end of file diff --git a/Mhf.Server/Files/Database/Script/schema_mariadb.sql b/Mhf.Server/Files/Database/Script/schema_mariadb.sql new file mode 100644 index 0000000..2d28159 --- /dev/null +++ b/Mhf.Server/Files/Database/Script/schema_mariadb.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS `setting` ( + `key` VARCHAR(255) NOT NULL, + `value` VARCHAR(255) NOT NULL, + PRIMARY KEY (`key`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8; + +CREATE TABLE IF NOT EXISTS `account` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(17) NOT NULL, + `normal_name` VARCHAR(17) NOT NULL, + `hash` VARCHAR(255) NOT NULL, + `mail` VARCHAR(255) NOT NULL, + `mail_verified` TINYINT(1) NOT NULL, + `mail_verified_at` DATETIME DEFAULT NULL, + `mail_token` VARCHAR(255) DEFAULT NULL, + `password_token` VARCHAR(255) DEFAULT NULL, + `state` INT(11) NOT NULL, + `last_login` DATETIME DEFAULT NULL, + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_account_name` (`name`), + UNIQUE KEY `uq_account_normal_name` (`normal_name`), + UNIQUE KEY `uq_account_mail` (`mail`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8; \ No newline at end of file diff --git a/Mhf.Server/Files/Database/Script/schema_sqlite.sql b/Mhf.Server/Files/Database/Script/schema_sqlite.sql new file mode 100644 index 0000000..e1db521 --- /dev/null +++ b/Mhf.Server/Files/Database/Script/schema_sqlite.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS `setting` ( + `key` TEXT NOT NULL, + `value` TEXT NOT NULL, + PRIMARY KEY (`key`) +); + +CREATE TABLE IF NOT EXISTS `account` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` TEXT NOT NULL, + `normal_name` TEXT NOT NULL, + `hash` TEXT NOT NULL, + `mail` TEXT NOT NULL, + `mail_verified` INTEGER NOT NULL, + `mail_verified_at` DATETIME DEFAULT NULL, + `mail_token` TEXT DEFAULT NULL, + `password_token` TEXT DEFAULT NULL, + `state` INTEGER NOT NULL, + `last_login` DATETIME DEFAULT NULL, + `created` DATETIME NOT NULL, + CONSTRAINT `uq_account_name` UNIQUE (`name`), + CONSTRAINT `uq_account_normal_name` UNIQUE (`normal_name`), + CONSTRAINT `uq_account_mail` UNIQUE (`mail`) +); \ No newline at end of file diff --git a/Mhf.Server/Files/mhf.pfx b/Mhf.Server/Files/mhf.pfx new file mode 100644 index 0000000..32a95ff Binary files /dev/null and b/Mhf.Server/Files/mhf.pfx differ diff --git a/Mhf.Server/Logging/MhfLogPacket.cs b/Mhf.Server/Logging/MhfLogPacket.cs new file mode 100644 index 0000000..3e3d883 --- /dev/null +++ b/Mhf.Server/Logging/MhfLogPacket.cs @@ -0,0 +1,46 @@ +using System; +using Mhf.Server.Packet; + +namespace Mhf.Server.Logging +{ + public class MhfLogPacket : MhfPacket + { + public MhfLogPacket(string clientIdentity, MhfPacket packet, MhfLogType logType) + : base(packet.Header, packet.Data.Clone()) + { + LogType = logType; + TimeStamp = DateTime.Now; + ClientIdentity = clientIdentity; + } + + public string ClientIdentity { get; } + public MhfLogType LogType { get; } + public DateTime TimeStamp { get; } + public string Hex => Data.ToHexString(' '); + public string Ascii => Data.ToAsciiString(true); + + public string ToLogText() + { + String log = $"{ClientIdentity} Packet Log"; + log += Environment.NewLine; + log += "----------"; + log += Environment.NewLine; + log += $"[{TimeStamp:HH:mm:ss}][Typ:{LogType}]"; + log += Environment.NewLine; + log += $"[Id:0x{Id:X2}|{Id}][BodyLen:{Data.Size}][{PacketIdName}]"; + log += Environment.NewLine; + log += Header.ToLogText(); + log += Environment.NewLine; + log += "ASCII:"; + log += Environment.NewLine; + log += Ascii; + log += Environment.NewLine; + log += "HEX:"; + log += Environment.NewLine; + log += Hex; + log += Environment.NewLine; + log += "----------"; + return log; + } + } +} diff --git a/Mhf.Server/Logging/MhfLogType.cs b/Mhf.Server/Logging/MhfLogType.cs new file mode 100644 index 0000000..acb0db5 --- /dev/null +++ b/Mhf.Server/Logging/MhfLogType.cs @@ -0,0 +1,10 @@ +namespace Mhf.Server.Logging +{ + public enum MhfLogType + { + PacketIn = 1, + PacketOut = 2, + PacketUnhandled = 3, + PacketError = 4 + } +} diff --git a/Mhf.Server/Logging/MhfLogger.cs b/Mhf.Server/Logging/MhfLogger.cs new file mode 100644 index 0000000..987f558 --- /dev/null +++ b/Mhf.Server/Logging/MhfLogger.cs @@ -0,0 +1,195 @@ +using System; +using Arrowgene.Services.Logging; +using Arrowgene.Services.Networking.Tcp; +using Mhf.Server.Model; +using Mhf.Server.Packet; +using Mhf.Server.Setting; + +namespace Mhf.Server.Logging +{ + public class MhfLogger : Logger + { + private MhfSetting _setting; + + public MhfLogger() : this(null) + { + } + + public MhfLogger(string identity, string zone = null) : base(identity, zone) + { + } + + public override void Initialize(string identity, string zone, object configuration) + { + base.Initialize(identity, zone, configuration); + _setting = configuration as MhfSetting; + if (_setting == null) + { + Error("Couldn't apply MhfLogger configuration"); + } + } + + public void Info(MhfClient client, string message, params object[] args) + { + Write(LogLevel.Info, null, $"{client.Identity} {message}", args); + } + + public void Info(MhfConnection connection, string message, params object[] args) + { + MhfClient client = connection.Client; + if (client != null) + { + Info(client, message, args); + return; + } + + Write(LogLevel.Info, null, $"{connection.Identity} {message}", args); + } + + public void Debug(MhfClient client, string message, params object[] args) + { + Write(LogLevel.Debug, null, $"{client.Identity} {message}", args); + } + + public void Error(MhfClient client, string message, params object[] args) + { + Write(LogLevel.Error, null, $"{client.Identity} {message}", args); + } + + public void Error(MhfConnection connection, string message, params object[] args) + { + MhfClient client = connection.Client; + if (client != null) + { + Error(client, message, args); + return; + } + + Write(LogLevel.Error, null, $"{connection.Identity} {message}", args); + } + + public void Exception(MhfClient client, Exception exception) + { + Write(LogLevel.Error, null, $"{client.Identity} {exception}"); + } + + public void Exception(MhfConnection connection, Exception exception) + { + MhfClient client = connection.Client; + if (client != null) + { + Exception(client, exception); + return; + } + + Write(LogLevel.Error, null, $"{connection.Identity} {exception}"); + } + + public void Info(ITcpSocket socket, string message, params object[] args) + { + Write(LogLevel.Info, null, $"[{socket.Identity}] {message}", args); + } + + public void Debug(ITcpSocket socket, string message, params object[] args) + { + Write(LogLevel.Debug, null, $"[{socket.Identity}] {message}", args); + } + + public void Error(ITcpSocket socket, string message, params object[] args) + { + Write(LogLevel.Error, null, $"[{socket.Identity}] {message}", args); + } + + public void Exception(ITcpSocket socket, Exception exception) + { + Write(LogLevel.Error, null, $"[{socket.Identity}] {exception}"); + } + + public void LogIncomingPacket(MhfClient client, MhfPacket packet) + { + if (_setting.LogIncomingPackets) + { + MhfLogPacket logPacket = new MhfLogPacket(client.Identity, packet, MhfLogType.PacketIn); + WritePacket(logPacket); + } + } + + public void LogIncomingPacket(MhfConnection connection, MhfPacket packet) + { + MhfClient client = connection.Client; + if (client != null) + { + LogIncomingPacket(client, packet); + return; + } + + if (!_setting.LogIncomingPackets) + { + return; + } + + MhfLogPacket logPacket = new MhfLogPacket(connection.Identity, packet, MhfLogType.PacketIn); + WritePacket(logPacket); + } + + public void LogUnknownIncomingPacket(MhfClient client, MhfPacket packet) + { + if (_setting.LogUnknownIncomingPackets) + { + MhfLogPacket logPacket = new MhfLogPacket(client.Identity, packet, MhfLogType.PacketUnhandled); + WritePacket(logPacket); + } + } + + public void LogUnknownIncomingPacket(MhfConnection connection, MhfPacket packet) + { + MhfClient client = connection.Client; + if (client != null) + { + LogUnknownIncomingPacket(client, packet); + return; + } + + if (!_setting.LogIncomingPackets) + { + return; + } + + MhfLogPacket logPacket = + new MhfLogPacket(connection.Identity, packet, MhfLogType.PacketUnhandled); + WritePacket(logPacket); + } + + public void LogOutgoingPacket(MhfClient client, MhfPacket packet) + { + if (_setting.LogOutgoingPackets) + { + MhfLogPacket logPacket = new MhfLogPacket(client.Identity, packet, MhfLogType.PacketOut); + WritePacket(logPacket); + } + } + + public void LogOutgoingPacket(MhfConnection connection, MhfPacket packet) + { + MhfClient client = connection.Client; + if (client != null) + { + LogOutgoingPacket(client, packet); + return; + } + + if (!_setting.LogIncomingPackets) + { + return; + } + + MhfLogPacket logPacket = new MhfLogPacket(connection.Identity, packet, MhfLogType.PacketOut); + WritePacket(logPacket); + } + + private void WritePacket(MhfLogPacket packet) + { + Write(LogLevel.Info, packet, packet.ToLogText()); + } + } +} diff --git a/Mhf.Server/Mhf.Server.csproj b/Mhf.Server/Mhf.Server.csproj new file mode 100644 index 0000000..f271834 --- /dev/null +++ b/Mhf.Server/Mhf.Server.csproj @@ -0,0 +1,25 @@ + + + netstandard2.1 + Mhf.Server + Monster Hunter Frontier Z Server + Mhf Team + Mhf.Server + $(Version) + Copyright © 2019 Mhf Team + 8 + + + + + + + + + + + PreserveNewest + Files\%(RecursiveDir)%(Filename)%(Extension) + + + diff --git a/Mhf.Server/MhfServer.cs b/Mhf.Server/MhfServer.cs new file mode 100644 index 0000000..27318c9 --- /dev/null +++ b/Mhf.Server/MhfServer.cs @@ -0,0 +1,132 @@ +/* + * This file is part of Mhf.Server + * + * Mhf.Server is a server implementation for the game "Monster Hunter Frontier Z". + * Copyright (C) 2019-2020 Mhf Team + * + * Github: https://github.com/sebastian-heinz/mhf-server + * + * Mhf.Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mhf.Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Mhf.Server. If not, see . + */ + +using Arrowgene.Services.Logging; +using Arrowgene.Services.Networking.Tcp.Server.AsyncEvent; +using Mhf.Server.Common.Instance; +using Mhf.Server.Database; +using Mhf.Server.Logging; +using Mhf.Server.Model; +using Mhf.Server.Packet; +using Mhf.Server.PacketHandler; +using Mhf.Server.Setting; +using Mhf.Server.Web; +using Mhf.Server.Web.Server.Kestrel; +using Mhf.Server.WebMiddlewares; +using Mhf.Server.WebRoutes; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server +{ + public class MhfServer + { + public MhfSetting Setting { get; } + public PacketRouter Router { get; } + public ClientLookup Clients { get; } + public IDatabase Database { get; } + public InstanceGenerator Instances { get; } + + public IFileProvider WebFileProvider { get; } + + private readonly QueueConsumer _authConsumer; + private readonly QueueConsumer _lobbyConsumer; + private readonly AsyncEventServer _authServer; + private readonly AsyncEventServer _lobbyServer; + private readonly WebServer _webServer; + private readonly MhfLogger _logger; + + public MhfServer(MhfSetting setting) + { + Setting = new MhfSetting(setting); + + LogProvider.Configure(Setting); + _logger = LogProvider.Logger(this); + + Instances = new InstanceGenerator(); + Clients = new ClientLookup(); + Router = new PacketRouter(); + Database = new MhfDatabaseBuilder().Build(Setting.DatabaseSetting); + + _authConsumer = new QueueConsumer(Setting, Setting.ServerSocketSettings); + _authConsumer.ClientDisconnected += AuthClientDisconnected; + _authServer = new AsyncEventServer( + Setting.ListenIpAddress, + Setting.AuthServerPort, + _authConsumer, + Setting.ServerSocketSettings + ); + + _lobbyConsumer= new QueueConsumer(Setting, Setting.ServerSocketSettings); + _lobbyConsumer.ClientDisconnected += LobbyClientDisconnected; + _lobbyServer = new AsyncEventServer( + Setting.ListenIpAddress, + Setting.LobbyServerPort, + _lobbyConsumer, + Setting.ServerSocketSettings + ); + + _webServer = new WebServer(Setting, new KestrelWebServer(Setting)); + + WebFileProvider = new PhysicalFileProvider(Setting.WebSetting.WebFolder); + + LoadPacketHandler(); + LoadWebRoutes(); + } + + private void AuthClientDisconnected(MhfConnection client) + { + } + + private void LobbyClientDisconnected(MhfConnection client) + { + } + + public void Start() + { + _authServer.Start(); + _webServer.Start(); + } + + public void Stop() + { + _authServer.Stop(); + _webServer.Stop(); + } + + private void LoadPacketHandler() + { + _authConsumer.AddHandler(new MsgHeadHandler(this)); + } + + private void LoadWebRoutes() + { + _webServer.AddRoute(new IndexRoute()); + _webServer.AddRoute(new AuthLauncherLoginRoute(WebFileProvider)); + _webServer.AddRoute(new AuthLauncherStartRoute(WebFileProvider)); + _webServer.AddRoute(new LauncherIndexRoute(WebFileProvider)); + _webServer.AddRoute(new MhfFileRoute(WebFileProvider)); + + // Middleware - Order Matters + _webServer.AddMiddleware(new StaticFileMiddleware("", WebFileProvider)); + } + } +} diff --git a/Mhf.Server/Model/Account.cs b/Mhf.Server/Model/Account.cs new file mode 100644 index 0000000..4a445b3 --- /dev/null +++ b/Mhf.Server/Model/Account.cs @@ -0,0 +1,25 @@ +using System; + +namespace Mhf.Server.Model +{ + public class Account + { + public int Id { get; set; } + public string Name { get; set; } + public string NormalName { get; set; } + public string Hash { get; set; } + public string Mail { get; set; } + public string MailToken { get; set; } + public string PasswordToken { get; set; } + public bool MailVerified { get; set; } + public DateTime? MailVerifiedAt { get; set; } + public AccountStateType State { get; set; } + public DateTime? LastLogin { get; set; } + public DateTime Created { get; set; } + + public Account() + { + Id = -1; + } + } +} diff --git a/Mhf.Server/Model/AccountStateType.cs b/Mhf.Server/Model/AccountStateType.cs new file mode 100644 index 0000000..3d6915b --- /dev/null +++ b/Mhf.Server/Model/AccountStateType.cs @@ -0,0 +1,11 @@ +namespace Mhf.Server.Model +{ + public enum AccountStateType + { + Banned = 0, + None = 0, + User = 1, + GameMaster = 50, + Admin = 100 + } +} diff --git a/Mhf.Server/Model/ClientLookup.cs b/Mhf.Server/Model/ClientLookup.cs new file mode 100644 index 0000000..47feeeb --- /dev/null +++ b/Mhf.Server/Model/ClientLookup.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + + namespace Mhf.Server.Model +{ + public class ClientLookup + { + private readonly List _clients; + + private readonly object _lock = new object(); + + public ClientLookup() + { + _clients = new List(); + } + + /// + /// Returns all Clients. + /// + public List GetAll() + { + lock (_lock) + { + return new List(_clients); + } + } + + /// + /// Adds a Client. + /// + public void Add(MhfClient client) + { + if (client == null) + { + return; + } + + lock (_lock) + { + _clients.Add(client); + } + } + + /// + /// Removes the Client from all lists and lookup tables. + /// + public void Remove(MhfClient client) + { + lock (_lock) + { + _clients.Remove(client); + } + } + + /// + /// Returns a Client by AccountId if it exists. + /// + public MhfClient GetByAccountId(int accountId) + { + List clients = GetAll(); + foreach (MhfClient client in clients) + { + if (client.Account.Id == accountId) + { + return client; + } + } + + return null; + } + } +} diff --git a/Mhf.Server/Model/DatabaseType.cs b/Mhf.Server/Model/DatabaseType.cs new file mode 100644 index 0000000..62c25f4 --- /dev/null +++ b/Mhf.Server/Model/DatabaseType.cs @@ -0,0 +1,7 @@ +namespace Mhf.Server.Model +{ + public enum DatabaseType + { + SQLite, + } +} diff --git a/Mhf.Server/Model/ISender.cs b/Mhf.Server/Model/ISender.cs new file mode 100644 index 0000000..247c6b9 --- /dev/null +++ b/Mhf.Server/Model/ISender.cs @@ -0,0 +1,9 @@ +using Mhf.Server.Packet; + +namespace Mhf.Server.Model +{ + public interface ISender + { + void Send(MhfPacket packet); + } +} diff --git a/Mhf.Server/Model/MhfClient.cs b/Mhf.Server/Model/MhfClient.cs new file mode 100644 index 0000000..db25a3e --- /dev/null +++ b/Mhf.Server/Model/MhfClient.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using Arrowgene.Services.Logging; +using Mhf.Server.Logging; +using Mhf.Server.Packet; + +namespace Mhf.Server.Model +{ + [DebuggerDisplay("{Identity,nq}")] + public class MhfClient : ISender + { + private readonly MhfLogger _logger; + + public MhfClient() + { + _logger = LogProvider.Logger(this); + Creation = DateTime.Now; + Identity = ""; + } + + public DateTime Creation { get; } + public string Identity { get; private set; } + public Account Account { get; set; } + public MhfConnection Connection { get; set; } + + + public void Send(MhfPacket packet) + { + Connection.Send(packet); + } + + public void UpdateIdentity() + { + Identity = ""; + if (Account != null) + { + Identity += $"[Acc:{Account.Id}:{Account.Name}]"; + return; + } + + if (Connection != null) + { + Identity += $"[Con:{Connection.Identity}]"; + } + } + + public void Close() + { + Connection?.Socket.Close(); + } + } +} diff --git a/Mhf.Server/Model/MhfConnection.cs b/Mhf.Server/Model/MhfConnection.cs new file mode 100644 index 0000000..6772bdf --- /dev/null +++ b/Mhf.Server/Model/MhfConnection.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Arrowgene.Services.Logging; +using Arrowgene.Services.Networking.Tcp; +using Mhf.Server.Logging; +using Mhf.Server.Packet; + +namespace Mhf.Server.Model +{ + public class MhfConnection : ISender + { + private readonly MhfLogger _logger; + + public MhfConnection(ITcpSocket clientSocket, PacketFactory packetFactory) + { + _logger = LogProvider.Logger(this); + Socket = clientSocket; + PacketFactory = packetFactory; + Client = null; + } + + public string Identity => Socket.Identity; + public ITcpSocket Socket { get; } + public PacketFactory PacketFactory { get; } + public MhfClient Client { get; set; } + + public List Receive(byte[] data) + { + List packets; + try + { + packets = PacketFactory.Read(data); + } + catch (Exception ex) + { + _logger.Exception(this, ex); + packets = new List(); + } + + return packets; + } + + public void Send(MhfPacket packet) + { + byte[] data; + try + { + data = PacketFactory.Write(packet); + } + catch (Exception ex) + { + _logger.Exception(this, ex); + return; + } + + // _logger.LogOutgoingPacket(this, packet); + Socket.Send(data); + } + } +} diff --git a/Mhf.Server/Packet/ClientHandler.cs b/Mhf.Server/Packet/ClientHandler.cs new file mode 100644 index 0000000..dfb8782 --- /dev/null +++ b/Mhf.Server/Packet/ClientHandler.cs @@ -0,0 +1,13 @@ +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public abstract class ClientHandler : Handler, IClientHandler + { + protected ClientHandler(MhfServer server) : base(server) + { + } + + public abstract void Handle(MhfClient client, MhfPacket packet); + } +} diff --git a/Mhf.Server/Packet/ClientHandlerDeserializer.cs b/Mhf.Server/Packet/ClientHandlerDeserializer.cs new file mode 100644 index 0000000..9e825ae --- /dev/null +++ b/Mhf.Server/Packet/ClientHandlerDeserializer.cs @@ -0,0 +1,22 @@ +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public abstract class ClientHandlerDeserializer : ClientHandler + { + private readonly IPacketDeserializer _deserializer; + + protected ClientHandlerDeserializer(MhfServer server, IPacketDeserializer deserializer) : base(server) + { + _deserializer = deserializer; + } + + public override void Handle(MhfClient client, MhfPacket requestPacket) + { + T request = _deserializer.Deserialize(requestPacket); + HandleRequest(client, request); + } + + public abstract void HandleRequest(MhfClient client, T request); + } +} diff --git a/Mhf.Server/Packet/ConnectionHandler.cs b/Mhf.Server/Packet/ConnectionHandler.cs new file mode 100644 index 0000000..2322562 --- /dev/null +++ b/Mhf.Server/Packet/ConnectionHandler.cs @@ -0,0 +1,13 @@ +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public abstract class ConnectionHandler : Handler, IConnectionHandler + { + protected ConnectionHandler(MhfServer server) : base(server) + { + } + + public abstract void Handle(MhfConnection client, MhfPacket packet); + } +} diff --git a/Mhf.Server/Packet/Handler.cs b/Mhf.Server/Packet/Handler.cs new file mode 100644 index 0000000..d842dc4 --- /dev/null +++ b/Mhf.Server/Packet/Handler.cs @@ -0,0 +1,30 @@ +using Arrowgene.Services.Logging; +using Mhf.Server.Database; +using Mhf.Server.Logging; +using Mhf.Server.Model; +using Mhf.Server.Setting; + +namespace Mhf.Server.Packet +{ + public abstract class Handler : IHandler + { + protected Handler(MhfServer server) + { + Logger = LogProvider.Logger(this); + Server = server; + Router = server.Router; + Database = server.Database; + Settings = server.Setting; + Clients = server.Clients; + } + + public abstract ushort Id { get; } + public virtual int ExpectedSize => QueueConsumer.NoExpectedSize; + protected MhfServer Server { get; } + protected MhfSetting Settings { get; } + protected MhfLogger Logger { get; } + protected PacketRouter Router { get; } + protected ClientLookup Clients { get; } + protected IDatabase Database { get; } + } +} diff --git a/Mhf.Server/Packet/IClientHandler.cs b/Mhf.Server/Packet/IClientHandler.cs new file mode 100644 index 0000000..9dd455f --- /dev/null +++ b/Mhf.Server/Packet/IClientHandler.cs @@ -0,0 +1,9 @@ +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public interface IClientHandler : IHandler + { + void Handle(MhfClient client, MhfPacket packet); + } +} diff --git a/Mhf.Server/Packet/IConnectionHandler.cs b/Mhf.Server/Packet/IConnectionHandler.cs new file mode 100644 index 0000000..3289831 --- /dev/null +++ b/Mhf.Server/Packet/IConnectionHandler.cs @@ -0,0 +1,9 @@ +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public interface IConnectionHandler : IHandler + { + void Handle(MhfConnection connection, MhfPacket packet); + } +} diff --git a/Mhf.Server/Packet/IHandler.cs b/Mhf.Server/Packet/IHandler.cs new file mode 100644 index 0000000..e6b2ab3 --- /dev/null +++ b/Mhf.Server/Packet/IHandler.cs @@ -0,0 +1,8 @@ +namespace Mhf.Server.Packet +{ + public interface IHandler + { + ushort Id { get; } + int ExpectedSize { get; } + } +} diff --git a/Mhf.Server/Packet/IPacketDeserializer.cs b/Mhf.Server/Packet/IPacketDeserializer.cs new file mode 100644 index 0000000..c30db91 --- /dev/null +++ b/Mhf.Server/Packet/IPacketDeserializer.cs @@ -0,0 +1,7 @@ +namespace Mhf.Server.Packet +{ + public interface IPacketDeserializer + { + T Deserialize(MhfPacket packet); + } +} diff --git a/Mhf.Server/Packet/IPacketSerializer.cs b/Mhf.Server/Packet/IPacketSerializer.cs new file mode 100644 index 0000000..d70c03d --- /dev/null +++ b/Mhf.Server/Packet/IPacketSerializer.cs @@ -0,0 +1,7 @@ +namespace Mhf.Server.Packet +{ + public interface IPacketSerializer + { + MhfPacket Serialize(T obj); + } +} diff --git a/Mhf.Server/Packet/MhfPacket.cs b/Mhf.Server/Packet/MhfPacket.cs new file mode 100644 index 0000000..6d8d63b --- /dev/null +++ b/Mhf.Server/Packet/MhfPacket.cs @@ -0,0 +1,56 @@ +using System; +using Arrowgene.Services.Buffers; + +namespace Mhf.Server.Packet +{ + public class MhfPacket + { + public static string GetPacketIdName(ushort id) + { + if (Enum.IsDefined(typeof(PacketId), id)) + { + PacketId authPacketId = (PacketId) id; + return authPacketId.ToString(); + } + + return null; + } + + private string _packetIdName; + + public MhfPacket(ushort id, IBuffer buffer) + { + Header = new PacketHeader(id); + Data = buffer; + } + + public MhfPacket(PacketHeader header, IBuffer buffer) + { + Header = header; + Data = buffer; + } + + public IBuffer Data { get; } + public ushort Id => Header.Id; + public PacketHeader Header { get; } + + public string PacketIdName + { + get + { + if (_packetIdName != null) + { + return _packetIdName; + } + + _packetIdName = GetPacketIdName(Id); + if (_packetIdName == null) + { + _packetIdName = $"ID_NOT_DEFINED_{Id}"; + } + + return _packetIdName; + } + } + } +} diff --git a/Mhf.Server/Packet/PacketFactory.cs b/Mhf.Server/Packet/PacketFactory.cs new file mode 100644 index 0000000..b67a1da --- /dev/null +++ b/Mhf.Server/Packet/PacketFactory.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using Arrowgene.Services.Buffers; +using Arrowgene.Services.Logging; +using Mhf.Server.Common; +using Mhf.Server.Logging; +using Mhf.Server.Setting; + +namespace Mhf.Server.Packet +{ + public class PacketFactory + { + public const int PacketHeaderSize = 14; + public const int InitPacketSize = 8; + public const uint CryptoInit = 995117; + + public static byte[] EncryptKey = new byte[] + { + 0x90, 0x51, 0x26, 0x25, 0x04, 0xBF, 0xCF, 0x4C, 0x92, 0x02, 0x52, 0x7A, 0x70, 0x1A, 0x41, 0x88, 0x8C, 0xC2, + 0xCE, 0xB8, 0xF6, 0x57, 0x7E, 0xBA, 0x83, 0x63, 0x2C, 0x24, 0x9A, 0x67, 0x86, 0x0C, 0xBE, 0x72, 0xFD, 0xB6, + 0x7B, 0x79, 0xB0, 0x22, 0x5A, 0x60, 0x5C, 0x4F, 0x49, 0xE2, 0x0E, 0xF5, 0x3A, 0x81, 0xAE, 0x11, 0x6B, 0xF0, + 0xA1, 0x01, 0xE8, 0x65, 0x8D, 0x5B, 0xDC, 0xCC, 0x93, 0x18, 0xB3, 0xAB, 0x77, 0xF7, 0x8E, 0xEC, 0xEF, 0x05, + 0x00, 0xCA, 0x4E, 0xA7, 0xBC, 0xB5, 0x10, 0xC6, 0x6C, 0xC0, 0xC4, 0xE5, 0x87, 0x3F, 0xC1, 0x82, 0x29, 0x96, + 0x45, 0x73, 0x07, 0xCB, 0x43, 0xF9, 0xF3, 0x08, 0x89, 0xD0, 0x99, 0x6A, 0x3B, 0x37, 0x19, 0xD4, 0x40, 0xEA, + 0xD7, 0x85, 0x16, 0x66, 0x1E, 0x9C, 0x39, 0xBB, 0xEE, 0x4A, 0x03, 0x8A, 0x36, 0x2D, 0x13, 0x1D, 0x56, 0x48, + 0xC7, 0x0D, 0x59, 0xB2, 0x44, 0xA3, 0xFE, 0x8B, 0x32, 0x1B, 0x84, 0xA0, 0x2E, 0x62, 0x17, 0x42, 0xB9, 0x9B, + 0x2B, 0x75, 0xD8, 0x1C, 0x3C, 0x4D, 0x76, 0x27, 0x6E, 0x28, 0xD3, 0x33, 0xC3, 0x21, 0xAF, 0x34, 0x23, 0xDD, + 0x68, 0x9F, 0xF1, 0xAD, 0xE1, 0xB4, 0xE7, 0xA6, 0x74, 0x15, 0x4B, 0xFA, 0x3D, 0x5F, 0x7C, 0xDA, 0x2F, 0x0A, + 0xE3, 0x7D, 0xC8, 0xB7, 0x12, 0x6F, 0x9E, 0xA9, 0x14, 0x53, 0x97, 0x8F, 0x64, 0xF4, 0xF8, 0xA2, 0xA4, 0x2A, + 0xD2, 0x47, 0x9D, 0x71, 0xC5, 0xE9, 0x06, 0x98, 0x20, 0x54, 0x80, 0xAA, 0xF2, 0xAC, 0x50, 0xD6, 0x7F, 0xD9, + 0xC9, 0xCD, 0x69, 0x46, 0x6D, 0x30, 0xB1, 0x58, 0x0B, 0x55, 0xD1, 0x5D, 0xD5, 0xBD, 0x31, 0xDE, 0xA5, 0xE4, + 0x91, 0x0F, 0x61, 0x38, 0xDF, 0xA8, 0xE6, 0x3E, 0x1F, 0x35, 0xED, 0xDB, 0x94, 0xEB, 0x09, 0x5E, 0x95, 0xFB, + 0xFC, 0xE0, 0x78, 0xFF + }; + + public static byte[] DecryptKey = new byte[] + { + 0x48, 0x37, 0x09, 0x76, 0x04, 0x47, 0xCC, 0x5C, 0x61, 0xF8, 0xB3, 0xE0, 0x1F, 0x7F, 0x2E, 0xEB, 0x4E, 0x33, + 0xB8, 0x7A, 0xBC, 0xAB, 0x6E, 0x8C, 0x3F, 0x68, 0x0D, 0x87, 0x93, 0x7B, 0x70, 0xF2, 0xCE, 0x9D, 0x27, 0xA0, + 0x1B, 0x03, 0x02, 0x97, 0x99, 0x58, 0xC5, 0x90, 0x1A, 0x79, 0x8A, 0xB2, 0xDD, 0xE6, 0x86, 0x9B, 0x9F, 0xF3, + 0x78, 0x67, 0xED, 0x72, 0x30, 0x66, 0x94, 0xAE, 0xF1, 0x55, 0x6A, 0x0E, 0x8D, 0x5E, 0x82, 0x5A, 0xDB, 0xC7, + 0x7D, 0x2C, 0x75, 0xAC, 0x07, 0x95, 0x4A, 0x2B, 0xD4, 0x01, 0x0A, 0xBD, 0xCF, 0xE1, 0x7C, 0x15, 0xDF, 0x80, + 0x28, 0x3B, 0x2A, 0xE3, 0xF9, 0xAF, 0x29, 0xEC, 0x8B, 0x19, 0xC0, 0x39, 0x6F, 0x1D, 0xA2, 0xDA, 0x65, 0x34, + 0x50, 0xDC, 0x98, 0xB9, 0x0C, 0xC9, 0x21, 0x5B, 0xAA, 0x91, 0x96, 0x42, 0xFE, 0x25, 0x0B, 0x24, 0xB0, 0xB5, + 0x16, 0xD6, 0xD0, 0x31, 0x57, 0x18, 0x88, 0x6D, 0x1E, 0x54, 0x0F, 0x62, 0x77, 0x85, 0x10, 0x3A, 0x44, 0xBF, + 0x00, 0xEA, 0x08, 0x3E, 0xF6, 0xFA, 0x59, 0xBE, 0xCD, 0x64, 0x1C, 0x8F, 0x71, 0xC8, 0xBA, 0xA3, 0x89, 0x36, + 0xC3, 0x83, 0xC4, 0xE8, 0xA9, 0x4B, 0xEF, 0xBB, 0xD1, 0x41, 0xD3, 0xA5, 0x32, 0x9E, 0x26, 0xDE, 0x81, 0x40, + 0xA7, 0x4D, 0x23, 0xB7, 0x13, 0x8E, 0x17, 0x73, 0x4C, 0xE5, 0x20, 0x05, 0x51, 0x56, 0x11, 0x9C, 0x52, 0xCA, + 0x4F, 0x7E, 0xB6, 0xD8, 0x49, 0x5D, 0x3D, 0xD9, 0x12, 0x06, 0x63, 0xE2, 0xC6, 0x9A, 0x69, 0xE4, 0xD5, 0x6C, + 0x92, 0xD7, 0xB1, 0xF5, 0x3C, 0xA1, 0xE7, 0xEE, 0xFD, 0xA6, 0x2D, 0xB4, 0xE9, 0x53, 0xF0, 0xA8, 0x38, 0xCB, + 0x6B, 0xF7, 0x45, 0xF4, 0x74, 0x46, 0x35, 0xA4, 0xD2, 0x60, 0xC1, 0x2F, 0x14, 0x43, 0xC2, 0x5F, 0xAD, 0xFB, + 0xFC, 0x22, 0x84, 0xFF + }; + + public static byte[] SharedCryptKey = new byte[] + { + 0xDD, 0xA8, 0x5F, 0x1E, 0x57, 0xAF, 0xC0, 0xCC, 0x43, 0x35, 0x8F, 0xBB, 0x6F, 0xE6, 0xA1, 0xD6, 0x60, 0xB9, + 0x1A, 0xAE, 0x20, 0x49, 0x24, 0x81, 0x21, 0xFE, 0x86, 0x2B, 0x98, 0xB7, 0xB3, 0xD2, 0x91, 0x01, 0x3A, 0x4C, + 0x65, 0x92, 0x1C, 0xF4, 0xBE, 0xDD, 0xD9, 0x08, 0xE6, 0x81, 0x98, 0x1B, 0x8D, 0x60, 0xF3, 0x6F, 0xA1, 0x47, + 0x24, 0xF1, 0x53, 0x45, 0xC8, 0x7B, 0x88, 0x80, 0x4E, 0x36, 0xC3, 0x0D, 0xC9, 0xD6, 0x8B, 0x08, 0x19, 0x0B, + 0xA5, 0xC1, 0x11, 0x4C, 0x60, 0xF8, 0x5D, 0xFC, 0x15, 0x68, 0x7E, 0x32, 0xC0, 0x50, 0xAB, 0x64, 0x1F, 0x8A, + 0xD4, 0x08, 0x39, 0x7F, 0xC2, 0xFB, 0xBA, 0x6C, 0xF0, 0xE6, 0xB0, 0x31, 0x10, 0xC1, 0xBF, 0x75, 0x43, 0xBB, + 0x18, 0x04, 0x0D, 0xD1, 0x97, 0xF7, 0x23, 0x21, 0x83, 0x8B, 0xCA, 0x25, 0x2B, 0xA3, 0x03, 0x13, 0xEA, 0xAE, + 0xFE, 0xF0, 0xEB, 0xFD, 0x85, 0x57, 0x53, 0x65, 0x41, 0x2A, 0x40, 0x99, 0xC0, 0x94, 0x65, 0x7E, 0x7C, 0x93, + 0x82, 0xB0, 0xB3, 0xE5, 0xC0, 0x21, 0x09, 0x84, 0xD5, 0xEF, 0x9F, 0xD1, 0x7E, 0xDC, 0x4D, 0xF5, 0x7E, 0xCD, + 0x45, 0x3C, 0x7F, 0xF5, 0x59, 0x98, 0xC6, 0x55, 0xFC, 0x9F, 0xA3, 0xB7, 0x74, 0xEE, 0x31, 0x98, 0xE6, 0xB7, + 0xBE, 0x26, 0xF4, 0x3C, 0x76, 0xF1, 0x23, 0x7E, 0x02, 0x4E, 0x3C, 0xD1, 0xC7, 0x28, 0x23, 0x73, 0xC4, 0xD9, + 0x5E, 0x0D, 0xA1, 0x80, 0xA5, 0xAA, 0x26, 0x0A, 0xA3, 0x44, 0x82, 0x74, 0xE6, 0x3C, 0x44, 0x27, 0x51, 0x0D, + 0x5F, 0xC7, 0x9C, 0xD6, 0x63, 0x67, 0xA5, 0x27, 0x97, 0x38, 0xFB, 0x2D, 0xD3, 0xD6, 0x60, 0x25, 0x83, 0x4D, + 0x37, 0x5B, 0x40, 0x59, 0x11, 0x77, 0x51, 0x11, 0x14, 0x18, 0x07, 0x63, 0xB1, 0x34, 0x3D, 0xB8, 0x60, 0x13, + 0xC2, 0xE8, 0x13, 0x82 + }; + + private readonly MhfLogger _logger; + private readonly MhfSetting _setting; + + private int _position; + private IBuffer _buffer; + private PacketHeader _header; + + private uint _readKeyRot; + private uint _sendKeyRot; + + private uint _sentPackets; + private ushort _combinedCheck; + + public PacketFactory(MhfSetting setting) + { + _logger = LogProvider.Logger(this); + _setting = setting; + _readKeyRot = CryptoInit; + _sendKeyRot = CryptoInit; + _sentPackets = 0; + _header = null; + Reset(); + } + + public byte[] Write(MhfPacket packet) + { + if (packet.Data.Size > ushort.MaxValue) + { + _logger.Error( + $"Packet Size: {packet.Data.Size} exceeds maximum size of {ushort.MaxValue} for PacketId: {packet.Id}"); + return null; + } + + byte[] data = packet.Data.GetAllBytes(); + + + byte keyRotDelta = 2; + // Update the rolling key index. + if (keyRotDelta != 0) + { + _sendKeyRot = keyRotDelta * (_sendKeyRot + 1); + } + + data = Encrypt(data, _sendKeyRot, + out ushort combinedCheck, out ushort check0, + out ushort check1, out ushort check2); + + packet.Header.DataSize = (ushort)data.Length; + packet.Header.Pf0 = (byte) (((packet.Header.DataSize >> 12) & 0xF3) | 3); + packet.Header.KeyRotDelta = keyRotDelta; + packet.Header.CombinedCheck = 0; + packet.Header.Check0 = check0; + packet.Header.Check1 = check1; + packet.Header.Check2 = check2; + + Console.WriteLine(packet.Header.ToLogText()); + + IBuffer buffer = BufferProvider.Provide(); + buffer.WriteByte(packet.Header.Pf0); + buffer.WriteByte(packet.Header.KeyRotDelta); + buffer.WriteInt16(packet.Header.Id, Endianness.Big); + buffer.WriteInt16(packet.Header.DataSize, Endianness.Big); + buffer.WriteInt16(packet.Header.CombinedCheck, Endianness.Big); + buffer.WriteInt16(packet.Header.Check0, Endianness.Big); + buffer.WriteInt16(packet.Header.Check1, Endianness.Big); + buffer.WriteInt16(packet.Header.Check2, Endianness.Big); + buffer.WriteBytes(data); + byte[] final = buffer.GetAllBytes(); + return final; + } + + public List Read(byte[] data) + { + List packets = new List(); + if (_buffer == null) + { + _buffer = BufferProvider.Provide(data); + } + else + { + _buffer.SetPositionEnd(); + _buffer.WriteBytes(data); + } + + _buffer.Position = _position; + + bool read = true; + while (read) + { + read = false; + + if (_header == null && _buffer.Size - _buffer.Position >= PacketHeaderSize) + { + byte pf0 = _buffer.ReadByte(); + byte keyRotDelta = _buffer.ReadByte(); + ushort id = _buffer.ReadUInt16(Endianness.Big); + ushort dataSize = _buffer.ReadUInt16(Endianness.Big); + ushort combinedCheck = _buffer.ReadUInt16(Endianness.Big); + ushort check0 = _buffer.ReadUInt16(Endianness.Big); + ushort check1 = _buffer.ReadUInt16(Endianness.Big); + ushort check2 = _buffer.ReadUInt16(Endianness.Big); + _header = new PacketHeader(id, pf0, keyRotDelta, dataSize, combinedCheck, check0, check1, check2); + Console.WriteLine(_header.ToLogText()); + // Update the rolling key index. + if (_header.KeyRotDelta != 0) + { + _readKeyRot = _header.KeyRotDelta * (_readKeyRot + 1); + } + } + + if (_header == null && _buffer.Size - _buffer.Position == InitPacketSize) + { + byte[] payload = _buffer.ReadBytes(InitPacketSize); + _logger.Debug($"Ignoring Data: {Util.ToHexString(payload, ' ')}"); + } + + if (_header != null && _buffer.Size - _buffer.Position >= _header.DataSize) + { + byte[] packetData = _buffer.ReadBytes(_header.DataSize); + packetData = Decrypt(packetData, _readKeyRot, + out ushort combinedCheck, out ushort check0, + out ushort check1, out ushort check2); + + IBuffer buffer = BufferProvider.Provide(packetData); + MhfPacket packet = new MhfPacket(_header, buffer); + packets.Add(packet); + _header = null; + read = _buffer.Position != _buffer.Size; + } + } + + if (_buffer.Position == _buffer.Size) + { + Reset(); + } + else + { + _position = _buffer.Position; + } + + return packets; + } + + private void Reset() + { + _header = null; + _position = 0; + _buffer = null; + } + + + public void Test() + { + byte[] me = Util.FromHexString( + "010201011E952C4F6D66366B414C5765553248535635755DD6002F0C75306D68662D672E6A70000C6C302E6D682D672E6A7000143130362E3138352E342E36313A3533333130002175A5E90000005DD0AE99000100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000028B3C424F44593E3C43455445523E3C53495A455F333E3C435F3E918D93FC8DB03490E7969C8DB09290AC82C582A882BD82DF82B58FAC9482663939399687947A957A81493C423E3C424F44593E91E631343789F18E906C8DD581758F9F82BF836C83528282E782CC92A790ED8FF38176936F988DD53C42523E3C424F44593E3C42523C424F44593E3C42523E3C424F44593C4C4546543E3C435F373E3C53495A5F323E8DA189F182C58DC58CE382C6C882E98EEB906C8DD581758F9F82BF6C835282A982E782CC92A790ED8FF3768A4A8DC381493C42523E3C424F443E3C42523E3C424F44593E3C4C45463E3C435F333E3C53495A455F323E81936F985E8DD58A4A8DC38AFA8AD4813C435F373E3C42523E3C424F44593E318C8E323093FA2890852920816020318C8E323793FA2890852983818393658369839383588A4A8E6E82DC82C5435F373E3C42523E3C424F44593E3C523E3C424F44593E91E631343789F1EB906C8DD582C582CD81418354815B6F815B918D93FC8DB0909482C9899EB682C481413C42523E3C424F44593E435F353E82A882BD82DF82B58FAC9482663939399687815E8A6C93BE9166DE90943130947B81413C42523E3C4244593E8F9F82BF9167834E834783586782F08343837883938367834E834758836782C682B582C4947A904D3C43373E82C882C73C42523E3C424F445993C195CA82C8964A8FDC82F092698A934982C982B2977088D382B582C48282E882DC82B781423C42523E3C424F593E3C42523E3C424F44593E836E83835E815B82CC8A46976C81418DC58C82CC8EEB906C8DD582C982BA82D08E89C182B582C482AD82BE82B382A28100000000000000000E2A64736D63000C00003D0000008181000000000000000000816A000000000000280000006900000000000021000000814900000000002F000000815E000000000000000000817B0000000000002600000095000000000000825000003100000000000082DA0000837B0000CE00DE00D9DE00837ADE0082D9814A837A814A00814A0000000082D700008378000000DE0082D6DE008377DE0082D6814A00814A8377814A0000000082C50000660000C300DE0082C4DE008365DE00C4814A8365814AC300814A81A7814AA7DE0089B3DE0089B3814A00000000D1000083720000CB00DE0082D0DE0071DE0082D0814A8371814ACB00814A00000082C7000083680000C400DE00C6DE008367DE0082C6814A8367814A00814A84B0DE0084B0814A84A5DE00A5814A0000000082CE0000836F000000DE0082CDDE00836EDE0094AADE00CD814A836E814ACA00814A94AA814A00000082C2DE00836400008363DE00C2814A8363814AC200DE00C200814AC3000082C1DE008362DE00AF00DE00C1814A8362814AAF00814A00000000D4000083750000CC00DE008394000000DE0082A4814A82A4DE008345DE0000DE0082A3DE0082D3DE008374DE0000814A0000000082C000008361000000DE0082BFDE008360DE0082BF814A60814AC100814A90E7814A90E7DE0000000082BE0000835F0000C000DE00BDDE00835EDE00975BDE0082BD814A5E814AC000814A975B814A00000000BC0000835D0000BF00DE0082BBDE005CDE0082BB814A835C814ABF00814A93DE008393814ADD00814ADD00DE008ADE00D800DE00D800814A838A814A00000082BA0000BE00DE0082B9DE005ADE0082B9814A835A814ABE00814A5B00000000000082B800008359000000DE0082B7DE008358DE0082B7814A58814ABD00814A0000000082B60000570000BC00DE0082B5DE008356DE00B5814A8356814ABC00814A00000000B4000083550000BB00DE0082B3DE0054DE0082B3814A8354814ABB00814A00000082B2000083530000BA00DE00B1DE008352DE0082B1814A8352814A00814A0000000082B000008351000000DE0082AFDE008350DE0082AF814A50814AB900814A8396DE008396814A00000082AE0000834F0000B800DE00ADDE00834EDE0082AD814A834E814A00814A0000000082AC0000834D000000DE0082ABDE00834CDE0082AB814A4C814AB700814A0000000082AA00004B0000B600DE008395DE00834ADE00A9DE0097CDDE008395814A834A814AA9814A97CD814AB600814A00000000F0000083920000A600000000000000ED0000838F0000DC000000838E000000000082EB0000838D0000DB000000A000008CFB00000000000082EA00008C0000DA0000000000000082E900008B0000D90000000000000082E800008A0000D80000000000000082E70000890000D70000000000000082E60000880000D6000000AE00000082E500008700000000000082E4000083860000000000AD00000082E300008385000000000082E2000083840000D400000000000082E100008383000000000000E0000083820000D300000000000000DF000083810000D20000004D00450000000082DE000083800000D100000000000082DD0000837E0000D000000000000082DC0000837D0000CF00000000000082D90000837A0000CE00000000000082D6000083770000CD00000000000082D3000083740000CC00000000000082D0000083710000CB00000000000082CD0000836E0000CA000000AA00000000000082CC0000836D00000000000000000082CB0000836C00000000000000000082CA0000836B00000000000000000082C90000836A000000000093F100000000000082C80000690000C50000000000000082C60000670000C400000084B0000084A5000000000082C4000083650000C3000000A7000089B300000000000082C20000630000C200000082C10000836200000000000000000082BF00008360000000000090E700000000000082BD00005E0000C0000000975B000000000000BB0000835C0000BF00000000000000B90000835A0000BE00000000000000B7000083580000BD00000000000000B5000083560000BC00000000000000B3000083540000BB00000000000000B1000083520000BA00000000000000AF000083500000B90000008396000000000082AD0000834E0000B800000000000082AB0000834C0000B700000000000082A90000834A0000B600000095000097CD00000000000082A80000490000B5000000AB00000082A700004800000000000082A6000083470000000000AA00000082A50000834600004800000000000082A4000083450000000000A900000082A300008344000000000082A2000083430000B200000000000082A100008342000000000000000000B1000000829F000082A000004000008341000000000000815B00005C0000815D0000816000002D0000007C0000B000000088EA000000000000000000825800000000000038000000570000000000003700000082560000000000360000008255000000000000000000825400000000000034000000530000000000003300000082520000000000320000008251000000000000DB0000837C0000CE00DF0082D9DF007ADF00837A818B82D9818BCE00818B00000082D8000083790000CD00DF00D6DF008377DF008377818B82D6818B00818B0000000082D500008376000000DF0082D3DF008374DF008374818BD3818BCC00818B0000000082D20000730000CB00DF0082D0DF008371DF0071818B82D0818BCB00818B00000000CF000083700000CA00DF0082CDDF006EDF00836E818B82CD818BCA00818BAADF0094AA814B00000000827900009A00005A0000007A00000083A4000000000082780000829900005900000000000083B20000845400008485000000000082770000829800005800000000000083B4000083D40000845600007E000084870000875D000000000000760000829700005700000077000000D6000084590000848A0000848B000000000082750000829600005600000000000083CB000083D200008758000000000082740000829500005500000000000083CA000081BE000000000000730000829400005400000074000000B1000083D10000845300008484000000000024000000530000007300000090000081E700008272000082930000000000827100008292000052000000000000846000008491000000000000700000829100005100000071000000000000826F0000829000005000000000000083AF000083CF00008451000082000000000000826E0000828F00000000006F000000819B000083AD0000CD0000844F00008480000081FC00005A000030000000824F0000000000006D0000828E00004E0000006E000000AB000083C50000DD00000082F1000093000000000000826C0000828D00000000006D00000083AA0000844D00007D000000000000826B0000828C00000000006C000000879800007C000000000000826A0000828B00004B00000000000083A8000083C80000844B00007B00000000000082690000828A00000000006A0000000000000082680000890000490000006900000083A700005400000000000082670000828800000000006800000083A50000844E00007E0000000000008265000082860000000000660000000000000082660000870000470000006700000000000000640000828500004500000065000000A3000083C3000084450000844600007500008476000081B80000000000006300008284000044000000640000000000004300000082830000630000005200008483000082620000818E000000000042000000826100008282000000000083C0000083A0000084420000720000848C0000848E000081F30000000000270000008166000000000000000000826000006100000082810000BF0000400000008197000084700000F0000084400000839F00000000000000000081680000000000002500000093000000000000000000006E616D0016000002000000937AFFFF97EAFFFF00FFFF0400000082B78B0182A9A801C6660182EBFC000000FFFF0200000073FFFF8F97FFFF0000FFFF03000000A2C401826D870282DBF3010000FFFF0000008266C2024100EF02826D870269A9024100EF020000FFFF04000000C46C0182A2C401826D870282B1970100FFFF0300000082DC380182BB8301B197010000FFFF0300000082BF7901BB830182B197010000FFFF02000000ADFFFF9390FFFF0000FFFF04000000CF170282A2C40182CF1702826D870200FFFF03000000965CFFFF82A9A80163FFFF0000FFFF0400000082B98701ADA00182EBFC0082B78B010000FFFF00000082CE4D0082BD7E01815BD201A2FFFF0000FFFF0500000082DC3801E80A0182D34401A700CB0182C8620100FFFF0400000082D5050282C27201B58F01815BD2010000FFFF04000000B1970182A9A80182A2C401826D870200FFFF0400000082C46C0182A2C4016D870282DBF3010000FFFF04000000D13700815BD20182BF790182ADA00100FFFF0200000090ABFFFF977EFFFF00FFFF0300000082C8620182BF7901B78B010000FFFF0400000082EBFC00E80A0182B19701826D87020000FFFF00000082C46C0182A2C40182DE3001DBF3010000FFFF0400000082C52A00A9A80182DC380182E70E010000FFFF00000082CF170282A2C40182B8A500E80A010000FFFF0400000082DC38016D870282A9A80182B78B010000FFFF0000004100EF028263D302826C910268AE02826D87028268AE0224005C02735302827164024100EF02827353026E7902827164020000FFFF06000000A8AE018B71FFFF976CFFFF918AFFFF6BFFFF8EBAFFFF0000FFFF02000000F2FFFF95A8FFFF0000FFFF04000000D2FFFF967BFFFF8C9BFFFF8E4FFFFF00FFFF0400000092D2FFFF967BFFFFB0FFFF8D4FFFFF0000FFFF04000000ACFFFF96ECFFFF8B60FFFF93BFFFFF00FFFF0300000082716402826C91027353020000FFFF040000009099FFFF59FFFF815BD20193BFFFFF0000FFFF00000092D2FFFF967BFFFF97C7FFFF4FFFFF0000FFFF0300000093A1FFFFAAFFFF9776FFFF0000FFFF040000005BD20190A3FFFF91D7FFFF94CDFFFF00FFFF050000008FACFFFF9388FFFF54FFFF91BEFFFF9859FFFF0000FFFF00000091E5FFFF9683FFFF0000FFFF0000008A6FFFFF90C1FFFF8DDCFFFF00FFFF020000009FAFFFFF92B0FFFF00FFFF020000009683FFFF96F2FFFF00FFFF0300000092AAFFFF9081FFFFABA4010000FFFF0300000082A9A801D505029859FFFF0000FFFF0100000079FFFF0000FFFF01000000817AFFFF00FFFF020000008941FFFF8C73FFFF00FFFF020000009675FFFF8B4EFFFF00FFFF0200000089BAFFFF979FFFFF00FFFF0600000082A8AE0182A4BD01DE3001905EFFFF979DFFFF8BB3FFFF00FFFF090000008269A9024100EF026F7002826AA0028268AE02826B99026B99028264C702827164020000FFFF0000008B43FFFF88E1FFFF82A2C40100FFFF040000008B43FFFF82BF7901AAE60082A2C4010000FFFF04000000ABA40182BF790182AAE60082A2C40100FFFF030000008B43FFFF8BB6FFFFA2C4010000FFFF0400000090B8FFFF5FFFFF9496FFFF8EE3FFFF0000FFFF000000926DFFFF8C62FFFF9278FFFFEA02010000FFFF0500000082BF7901A6B50182A8AE0182ADA00182EA020100FFFF0200000096DAFFFF88C3FFFF00FFFF0200000096BEFFFF96D3FFFF00FFFF05000000A700CB0182ABA401DF2B0182ADA00182E70E010000FFFF00000082EBFC00826D870282CF1702E80A010000FFFF02000000E19AFFFF61FFFF0000FFFF0300000082E70E01A2C4019561FFFF0000FFFF0200000092FFFF9273FFFF0000FFFF01000000EAFFFF0000FFFF0300000082BF79016D870282CE4D000000FFFF01000000FBFFFF0000FFFF0200000098FBFFFF43FFFF0000FFFF02000000936AFFFF45FFFF0000FFFF020000008CEEFFFF48FFFF0000FFFF02000000E6CBFFFFAFFFFF0000FFFF02000000E271FFFFBDFFFF0000FFFF0200000094F1FFFF6CFFFF0000FFFF030000009056FFFFBDFFFF96AFFFFF0000FFFF04000000C1FFFF8EEAFFFF9594FFFF978EFFFF00FFFF0500000082CE4D0082A9A801BF790182E61201826D87020000FFFF00000082BF790182E61201826D8702F6FFFF0000FFFF0500000082C66601E9060182B197019597FFFF9843FFFF00FFFF0400000082C6660182E90601B197018FECFFFF0000FFFF0300000000CB0182C8620182E906010000FFFF00000082A4BD01826D870282B1970100FFFF0200000096D1FFFF9382FFFF00FFFF030000008D95FFFF826D8702DA18000000FFFF040000009573FFFFC2FFFF9047FFFF96AFFFFF0000FFFF0000009484FFFF8F97FFFF0000FFFF00000088FAFFFF9484FFFF0000FFFF0000008BADFFFF8AADFFFF0000FFFF00000082E2200182E80A0182DC38016D87020000FFFF0300000082DC38016D870282B197010000FFFF03000000A8AE0182DF2B0182B197010000FFFF00000082BF7901826D870282B1970100FFFF0300000082BF7901826D8702DBF3010000FFFF0300000082BF79016D870282DA18000000FFFF04000000B9870182C2720182ADA00182B78B0100FFFF0300000024005C028264C7027730020000FFFF020000009484FFFF74FFFF0000FFFF020000009483FFFF74FFFF0000FFFF0300000082EA0201A2C40182D505020000FFFF04000000B78B0182D8FC0182E9060182DC380100FFFF0500000082ADA00182E80A01C6660182E80A0182B78B010000FFFF00000082D3440182A6B50182E70E01BF790182A8AE010000FFFF03000000B4B700815BD20196CBFFFF0000FFFF00000082B4B700815BD20182DF2B016D87020000FFFF0400000082B987016D870282B8A50082E80A010000FFFF00000082A8AE0182C8620182C95D015BD2010000FFFF030000008FE1FFFF51FFFF8ED2FFFF0000FFFF0400000065BD0282744C024300D802826AA00200FFFF0400000082D34401A700CB01C2720182ADA0010000FFFF05000000BF790182E22001826D870282B19701EBFC000000FFFF050000004200E00268AE02827353024300D8028267B50200FFFF040000004300D80282744C026D8702827353020000FFFF050000006F70028264C702826D87028268AE02005C020000FFFF0300000082D8FC01C95D0182B78B010000FFFF060000007544024100EF028266C2028268AE026D87024100EF020000FFFF03000000CE4D0082ACDD0082C862010000FFFF00000082D46700A700CB0182ACDD00C862010000FFFF05000000826F7002744C0224005C0224005C028278280200FFFF0200000090B8FFFF8974FFFF00FFFF020000008E80FFFF82CB550100FFFF020000008E45FFFF82B78B0100FFFF0100000095B3FFFF0000FFFF0000008998FFFF95A8FFFF0000FFFF00000082BF7901826D870282DBF301B197010000FFFF0400000082DBF301B1970182BF7901826D87020000FFFF00000082B3930182B987018E71FFFF00FFFF020000008941FFFF96D1FFFF00FFFF020000008BCAFFFF8BE0FFFF00FFFF0400000082A9A80182D50502B19701826D87020000FFFF04000000A9A80182D5050282B1970182BB830100FFFF0400000082A9A80182D505026D870282B197010000FFFF0600000000D8024100EF02826F70024300D8026E7902826C91020000FFFF0600000000D8024100EF02826F70024300D8026E7902826D87020000FFFF0600000000D8024100EF02826F7002826AA0026E7902826C91020000FFFF0600000000D8024100EF02826F7002826AA0026E7902826D87020000FFFF060000006AA0024100EF02826F70024300D8026E7902826C91020000FFFF060000006AA0024100EF02826F70024300D8026E7902826D87020000FFFF060000006AA0024100EF02826F7002826AA0026E7902826C91020000FFFF060000006AA0024100EF02826F7002826AA0026E7902826D87020000FFFF0900000066C2024100EF0282DF2B01826C910200EF0224005C02827353028264C7027164020000FFFF0A0000008266C20200EF0282DF2B0124005C0282744C026F7002826F7002826E7902827164027353020000FFFF0600000024005C0278280224005C02827353028264C7026C91020000FFFF0400000082B58F01B78B0182C46C0182DE30010000FFFF0000008264C702827544028264C7026D8702827353020000FFFF04000000A2C40182D72100826D870282C6660100FFFF0500000024005C028273530200EF028265BD028265BD020000FFFF00000082B78B0182BD7E0182C27201D344010000FFFF08000000826E790265BD028265BD028268AE024300D80268AE024100EF02826B99020000FFFF00000082A8AE0182D3440182A2C401B58F0182E2200182E906010000FFFF00000024005C0282744C02826F70026F7002826E7902827164028273530200FFFF0400000082B3930182DBF3015BD20182C666010000FFFF0500000000EF028263D302826C91028268AE026D87020000FFFF04000000A700CB01C7400082DD3401826D87020000FFFF000000A700CB0182C7400082DD3401BB83010000FFFF04000000A700CB01C7400082DD340182C95D010000FFFF000000A700CB0182C7400082DD3401C95D010000FFFF02000000895EFFFF63FFFF0000FFFF0400000082A4BD016D870282A6B50182A2C4010000FFFF0000008AC7FFFF979DFFFF0000FFFF00000082A9A801826D870282E80A0100FFFF030000008AD6FFFF8C57FFFFD2FFFF0000FFFF030000008A4AFFFFADFFFF8ED2FFFF0000FFFF04000000A8AE01926DFFFF82E70E0182B9870100FFFF0400000082A8AE0182B58F01E70E0182B987010000FFFF0200000090FFFF926DFFFF0000FFFF03000000D4FFFF88C0FFFF9577FFFF0000FFFF00000082E2200182E80A0182BF79016D87020000FFFF0300000082E22001ADA00182B4B7000000FFFF04000000D2FFFF967BFFFF8F74FFFF8D4FFFFF00FFFF0300000096D8FFFF967BFFFF63D3020000FFFF0400000096D8FFFF7BFFFF97B4FFFF8CC8FFFF0000FFFF0000008B7BFFFF89BAFFFF4100EF026F70020000FFFF040000008B7BFFFFBAFFFF8B50FFFF8EF7FFFF0000FFFF000000826C91028267B5028265BD025BD2018266C20282BF7901815BD201DE30010000FFFF06000000826C910267B5028265BD0282BF7901815BD201DE30010000FFFF040000008B43FFFFAED40082E9060182A2C4010000FFFF00000082ABA40182AED40082E90601A2C4010000FFFF0200000090B8FFFF96FFFF0000FFFF0200000088FAFFFF90FFFF0000FFFF020000009641FFFF50FFFF0000FFFF03000000945DFFFF8CFFFF898AFFFF0000FFFF0200000054FFFF93AAFFFF0000FFFF03000000DF2B0182ADA00182E70E010000FFFF00000093F7FFFF965FFFFF0000FFFF000000956EFFFF93FBFFFF0000FFFF00000082BE800082C2720182BF7901EDF70082A2C40182D344010000FFFF0000009857FFFF0000FFFF03000000C27201826D870282DA18000000FFFF0000008B68FFFF82E80A010000FFFF0000009181FFFF9852FFFF0000FFFF00000082A2C40182DC380182E70E01BF790182A8AE010000FFFF05000000A8AE0182C8620182C95D0182B78B01C666010000FFFF0200000088A4FFFF74FFFF0000FFFF020000008AE7FFFFCBFFFF0000FFFF0200000095D0FFFF5BFFFF0000FFFF020000009790FFFFF0FFFF0000FFFF0300000082B98701DE300182B58F010000FFFF030000004FFFFF82C2720182EBFC000000FFFF000000E3E8FFFF96E5FFFF0000FFFF00000093F7FFFF92D9FFFF0000FFFF00000090ABFFFF8AB4FFFF91D1FFFF00FFFF0500000082A8AE01815BD201AAE60082B8A50082DE30010000FFFF0000008EA9FFFF88D4FFFF0000FFFF00000093FBFFFF97D6FFFF0000FFFF0000009278FFFF9852FFFF0000FFFF0000008DD7FFFF96AFFFFF0000FFFF0000008B87FFFF96AFFFFF0000FFFF00000094D8FFFF91B0FFFF0000FFFF000000966BFFFF914EFFFF0000FFFF00000093ECFFFF914EFFFF0000FFFF0000008E4FFFFF8D91FFFF906CFFFF00FFFF0200000097BDFFFF904AFFFF00FFFF0200000096B2FFFF90B8FFFF00FFFF010000008FA8FFFF0000FFFF0000008FA9FFFF9577FFFF0000FFFF0000008F97FFFF8941FFFF0000FFFF0000008EE8FFFF88FAFFFF0000FFFF0000008D67FFFF96D1FFFF906CFFFF00FFFF020000009849FFFF8F95FFFF00FFFF020000008987FFFF8CF0FFFF00FFFF0400000082ADA00182EBFC006D870282DA18000000FFFF0300000000CB0182DF2B018CF6FFFF0000FFFF00000082A2C40182BD7E018CF6FFFF00FFFF040000009553FFFF8AD1FFFFC52A0082D467000000FFFF04000000E90601826D870282D8FC01826D870200FFFF020000008941FFFF9594FFFF00FFFF020000008941FFFF904FFFFF00FFFF0200000088FAFFFF8D73FFFF00FFFF0400000082BF7901826D8702BF7901826D87020000FFFF020000006AFFFF8DAAFFFF0000FFFF05000000A8AE018F95FFFF82AF9B018EEBFFFF6CFFFF0000FFFF0300000082BF79016D870296D1FFFF0000FFFF020000004EFFFF906CFFFF0000FFFF03000000ADA001826D870282C95D010000FFFF0000009099FFFF8959FFFF826F700200FFFF0200000093B6FFFF92E5FFFF00FFFF0200000095EFFFFF8C73FFFF00FFFF020000008BE0FFFF8BCAFFFF00FFFF0400000082A8AE0182C27201CF170282A2C4010000FFFF03000000BF790182ADA00182D137000000FFFF000000A700CB0182C8620182E9060100FFFF03000000A700CB0182CA5901B78B010000FFFF0200000093FBFFFFF1FFFF0000FFFF01000000E453FFFF00FFFF0200000090C2FFFF8AADFFFF00FFFF0200000096ECFFFF8AADFFFF00FFFF0200000095D6FFFF94E9FFFF00FFFF020000008FACFFFF95D6FFFF00FFFF0200000091E5FFFF95D6FFFF00FFFF0300000082D1370082C27201BF79010000FFFF0300000082A4BD016D870282BF79010000FFFF0200000070FFFF906CFFFF0000FFFF0200000073FFFF8ABFFFFF0000FFFF04000000B9870182ADA00182CD4C0182E70E0100FFFF0400000082744C02826D87026AA002826E79020000FFFF04000000BF7901826D870282A9A80182B78B0100FFFF0400000082CF1702826D8702C46C0182A2C4010000FFFF02000000D6FFFF8AEDFFFF0000FFFF04000000ABA401826D870282BD7E0182DC380100FFFF05000000826C91024100EF026D8702826AA002826E79020000FFFF00000082B78B0182C66601815BD201A9A801815BD2010000FFFF03000000BF7901815BD20182C666010000FFFF00000082AAE600826D870282B6AE00E220010000FFFF000000006D736700080000020000009675FFFF8B4EFFFF00FFFF030000008B43FFFF88E1FFFFA2C4010000FFFF040000008B43FFFFBF790182AAE60082A2C4010000FFFF00000082ABA40182BF790182AAE600A2C4010000FFFF030000008B43FFFFB6FFFF82A2C4010000FFFF04000000B8FFFF905FFFFF9496FFFF8EE3FFFF00FFFF04000000926DFFFF8C62FFFF78FFFF82EA02010000FFFF05000000BF790182A6B50182A8AE0182ADA001EA02010000FFFF0200000096DAFFFFC3FFFF0000FFFF0200000096BEFFFFD3FFFF0000FFFF05000000A700CB01ABA40182DF2B0182ADA00182E70E0100FFFF0400000082EBFC00826D8702CF170282E80A010000FFFF020000009AFFFF9561FFFF0000FFFF03000000E70E0182A2C4019561FFFF0000FFFF0000009492FFFF9273FFFF0000FFFF000000E6EAFFFF0000FFFF03000000BF7901826D870282CE4D000000FFFF00000098FBFFFF0000FFFF02000000FBFFFF9943FFFF0000FFFF020000006AFFFF8E45FFFF0000FFFF02000000EEFFFF9048FFFF0000FFFF02000000CBFFFF96AFFFFF0000FFFF0200000071FFFF91BDFFFF0000FFFF02000000F1FFFF906CFFFF0000FFFF0300000056FFFF95BDFFFF96AFFFFF0000FFFF00000093C1FFFF8EEAFFFF9594FFFF8EFFFF0000FFFF0500000082CE4D00A9A80182BF790182E61201826D870200FFFF0400000082BF790182E612016D87028CF6FFFF0000FFFF05000000C6660182E9060182B197019597FFFF43FFFF0000FFFF0400000082C66601E9060182B197018FECFFFF0000FFFF00000096D1FFFF9382FFFF0000FFFF0000008D95FFFF826D870282DA180000FFFF040000009573FFFF89C2FFFF47FFFF96AFFFFF0000FFFF0200000084FFFF8F97FFFF0000FFFF02000000FAFFFF9484FFFF0000FFFF02000000ADFFFF8AADFFFF0000FFFF04000000E2200182E80A0182DC3801826D870200FFFF0300000082DC3801826D8702B197010000FFFF0300000082A8AE01DF2B0182B197010000FFFF03000000BF7901826D870282DBF3010000FFFF00000082BF7901826D870282DA180000FFFF0400000082B9870182C27201ADA00182B78B010000FFFF03000000005C028264C702827730020000FFFF0000009484FFFF8F74FFFF0000FFFF0000009483FFFF8F74FFFF0000FFFF00000082EA020182A2C40182D5050200FFFF0400000082B78B0182D8FC01E9060182DC38010000FFFF05000000ADA00182E80A0182C6660182E80A01B78B010000FFFF0500000082D34401A6B50182E70E0182BF790182A8AE0100FFFF0300000082B4B700815BD201CBFFFF0000FFFF0400000082B4B7005BD20182DF2B01826D87020000FFFF00000082B98701826D870282B8A500E80A010000FFFF0400000082A8AE01C8620182C95D01815BD2010000FFFF0000008265BD0282744C024300D8026AA0020000FFFF0400000082D3440100CB0182C2720182ADA0010000FFFF00000082BF790182E22001826D8702B1970182EBFC000000FFFF0500000000E0028268AE02827353024300D80267B5020000FFFF040000004300D802744C02826D8702827353020000FFFF000000826F70028264C702826D870268AE0224005C020000FFFF03000000D8FC0182C95D0182B78B010000FFFF000000827544024100EF028266C20268AE02826D87024100EF020000FFFF00000082CE4D0082ACDD0082C8620100FFFF0400000082D46700A700CB01ACDD0082C862010000FFFF050000006F700282744C0224005C0224005C027828020000FFFF0200000090B8FFFF74FFFF0000FFFF0400000082BF79016D870282DBF30182B197010000FFFF00000088FAFFFF9790FFFF0000FFFF0000009641FFFF9550FFFF0000FFFF0000008B54FFFF93AAFFFF0000FFFF00000093F7FFFF965FFFFF0000FFFF000000956EFFFF93FBFFFF0000FFFF00000082BE800082C2720182BF7901EDF70082A2C40182D344010000FFFF0000009181FFFF9852FFFF0000FFFF00000082A2C40182DC380182E70E01BF790182A8AE010000FFFF05000000A8AE0182C8620182C95D0182B78B01C666010000FFFF0200000088A4FFFF74FFFF0000FFFF020000008AE7FFFFCBFFFF0000FFFF020000009790FFFFF0FFFF0000FFFF02000000E3E8FFFFE5FFFF0000FFFF0200000093F7FFFFD9FFFF0000FFFF0300000090ABFFFFB4FFFF91D1FFFF0000FFFF05000000A8AE01815BD20182AAE60082B8A500DE30010000FFFF020000008EA9FFFFD4FFFF0000FFFF0200000093FBFFFFD6FFFF0000FFFF020000009278FFFF52FFFF0000FFFF0200000097BDFFFF4AFFFF0000FFFF0200000096B2FFFFB8FFFF0000FFFF010000008FA8FFFF00FFFF020000008FA9FFFF9577FFFF00FFFF020000008F97FFFF8941FFFF00FFFF020000008EE8FFFF88FAFFFF00FFFF020000008987FFFF8CF0FFFF00FFFF0400000082E90601826D8702D8FC01826D87020000FFFF0200000041FFFF9594FFFF0000FFFF0200000041FFFF904FFFFF0000FFFF02000000FAFFFF8D73FFFF0000FFFF020000006AFFFF8DAAFFFF0000FFFF03000000BF7901826D870296D1FFFF0000FFFF00000093B6FFFF92E5FFFF0000FFFF00000095EFFFFF8C73FFFF0000FFFF00000093FBFFFF8EF1FFFF0000FFFF000000E453FFFF0000FFFF02000000C2FFFF8AADFFFF0000FFFF02000000ECFFFF8AADFFFF0000FFFF00000000104E200031736B30302445532E537457795159485A70547677724162734E6D685661334F4F6576467733617364325A3430632D00CA1100014E200000FD8D2F000000003DDFCC945DD4AD5CDDCA90020000000A00000004080A030902080507"); + + + byte[] packetData = Util.FromHexString( + "444C54534B45595349474E3A313030007465737400736B3030242E546676664B2E487646356E6C317436624338555843536352766538524249704D4D4E395537524C786A6B2D0000"); + + + byte[] packetData1 = Enc(packetData, (byte) (995118 & 0xFF)); + Console.WriteLine("Enc " + Util.ToHexString(packetData1, ' ')); + + byte[] packetData2 = Encrypt(packetData, 2, + out ushort combinedCheck, out ushort check0, + out ushort check1, out ushort check2); + Console.WriteLine("Enc " + Util.ToHexString(packetData2, ' ')); + + + // packetData = Decrypt(packetData); + // Console.WriteLine("Dec " + Util.ToHexString(packetData, ' ')); + // Console.WriteLine("Dec " + Util.ToAsciiString(packetData, true)); + } + + private byte[] Decrypt(byte[] input, uint key, out ushort cc, out ushort c0, out ushort c1, out ushort c2) + { + uint size = (uint) input.Length; + byte[] output = new byte[size]; + + byte unkCryptkeyRotArg = (byte) ((key >> 1) % 999983); + uint unkDerivedCryptkeyRot = (uint) (size * (unkCryptkeyRotArg + 1)); + + byte sharedBufIdx = 1; + uint accumulator0 = 0; + uint accumulator1 = 0; + uint accumulator2 = 0; + + for (int i = 0; i < size; i++) + { + // Do the decryption for this iteration + byte oldSharedBufIdx = sharedBufIdx; + byte tIdx = (byte) (input[i] ^ SharedCryptKey[sharedBufIdx]); + uint decKeyByte = DecryptKey[tIdx]; + sharedBufIdx = (byte) ((unkDerivedCryptkeyRot >> 10) ^ decKeyByte); + + // Update the checksum accumulators. + accumulator0 = (uint) (accumulator0 + (tIdx << (i & 7))); + accumulator1 = accumulator1 + decKeyByte; + accumulator2 = (uint) (accumulator2 + (oldSharedBufIdx * sharedBufIdx)); + + // Append the output. + output[i] = sharedBufIdx; + + // Update the key pos for next iteration. + unkDerivedCryptkeyRot = 0x4FD * (unkDerivedCryptkeyRot + 1); + } + + ushort combinedCheck = (ushort) (accumulator1 + (accumulator0 >> 1) + (accumulator2 >> 2)); + ushort check0 = (ushort) (accumulator0 ^ (accumulator0 >> 16)); + ushort check1 = (ushort) (accumulator1 ^ (accumulator1 >> 16)); + ushort check2 = (ushort) (accumulator2 ^ (accumulator2 >> 16)); + + cc = combinedCheck; + c0 = check0; + c1 = check1; + c2 = check2; + return output; + } + + private byte[] Encrypt(byte[] input, uint key, out ushort cc, out ushort c0, out ushort c1, out ushort c2) + { + uint size = (uint) input.Length; + byte[] output = new byte[size]; + + byte unkCryptkeyRotArg = (byte) ((key >> 1) % 999983); + uint unkDerivedCryptkeyRot = (uint) (size * (unkCryptkeyRotArg + 1)); + + byte sharedBufIdx = 1; + uint accumulator0 = 0; + uint accumulator1 = 0; + uint accumulator2 = 0; + + for (int i = 0; i < size; i++) + { + // Do the encryption for this iteration + byte encKeyIdx = (byte) ((unkDerivedCryptkeyRot >> 10) ^ input[i]); + unkDerivedCryptkeyRot = 0x4FD * (unkDerivedCryptkeyRot + 1); + byte encKeyByte = EncryptKey[encKeyIdx]; + + // Update the checksum accumulators. + accumulator2 = (uint) (accumulator2 + (sharedBufIdx * input[i])); + accumulator1 = accumulator1 + encKeyIdx; + accumulator0 = (uint) (accumulator0 + (encKeyByte << (i & 7))); + + // Append the output. + output[i] = (byte) (SharedCryptKey[sharedBufIdx] ^ encKeyByte); + + // Update the shared_buf_idx for the next iteration. + sharedBufIdx = input[i]; + } + + ushort combinedCheck = (ushort) (accumulator1 + (accumulator0 >> 1) + (accumulator2 >> 2)); + ushort check0 = (ushort) (accumulator0 ^ (accumulator0 >> 16)); + ushort check1 = (ushort) (accumulator1 ^ (accumulator1 >> 16)); + ushort check2 = (ushort) (accumulator2 ^ (accumulator2 >> 16)); + + cc = combinedCheck; + c0 = check0; + c1 = check1; + c2 = check2; + return output; + } + + byte[] Enc(byte[] input, byte a5) + { + byte v12; + uint v13; + uint v14; + uint inputSize = (uint) input.Length; + byte outputIndex = 0; + byte inputIndex = 0; + uint v5 = inputSize * (a5 + 1u); + uint v6 = 0; + uint v7 = 0; + uint v8 = 0; + uint v9 = 0; + byte v10 = 1; + uint v19 = 0; + uint v18 = 0; + uint v17 = 0; + byte[] output = new byte[inputSize]; + if (inputSize > 0) + { + do + { + v12 = input[inputIndex]; + v19 += (uint) v10 * input[inputIndex]; + v13 = (byte) ((v5 >> 10) ^ input[inputIndex]); + v18 += v13; + v13 = ReplaceByte(0, v13, EncryptKey[v13]); + v14 = (uint) ((byte) v13 << ((int) v9 & 7)); + v9++; + v5 = 1277 * (v5 + 1); + v17 += v14; + output[outputIndex] = (byte) (SharedCryptKey[v10] ^ v13); + inputIndex = (byte) (outputIndex + 1); + v10 = v12; + ++outputIndex; + } while (v9 < inputSize); + + v6 = v19; + v7 = v18; + v8 = v17; + } + + uint a6 = v6 ^ (v6 >> 16); + uint a4 = v7 ^ (v7 >> 16); + uint result = (uint) (v7 + ((int) v8 >> 1) + ((int) v6 >> 2)); + uint a2 = v8 ^ (v8 >> 16); + ushort check3 = (ushort) a6; + ushort check2 = (ushort) a4; + ushort check1 = (ushort) a2; + ushort res = (ushort) result; + byte resB = (byte) result; + + return output; + } + + uint ReplaceByte(uint index, uint value, byte replaceByte) + { + var shiftBits = 8 * index; + var mask = ~(0xff << (int) shiftBits); + return (uint) (value & mask | (replaceByte << (int) shiftBits)); + } + } +} diff --git a/Mhf.Server/Packet/PacketHeader.cs b/Mhf.Server/Packet/PacketHeader.cs new file mode 100644 index 0000000..a12ef04 --- /dev/null +++ b/Mhf.Server/Packet/PacketHeader.cs @@ -0,0 +1,39 @@ +namespace Mhf.Server.Packet +{ + public class PacketHeader + { + public PacketHeader(ushort id) : this(id, 0, 0, 0, 0, 0, 0, 0) + { + } + + public PacketHeader(ushort id, byte pf0, byte keyRot, ushort dataSize, ushort combinedCheck, ushort check0, + ushort check1, + ushort check2) + { + Id = id; + Pf0 = pf0; + KeyRotDelta = keyRot; + DataSize = dataSize; + CombinedCheck = combinedCheck; + Check0 = check0; + Check1 = check1; + Check2 = check2; + } + + public ushort Id { get; set; } + public byte Pf0 { get; set; } + public byte KeyRotDelta { get; set; } + public ushort DataSize { get; set; } + public ushort CombinedCheck { get; set; } + public ushort Check0 { get; set; } + public ushort Check1 { get; set; } + public ushort Check2 { get; set; } + + + public string ToLogText() + { + return + $"[Pf0:0x{Pf0:X2}|KeyRotDelta:0x{KeyRotDelta:X2}|Id:0x{Id:X2}|DataSize:0x{DataSize:X2}|CombinedCheck:0x{CombinedCheck:X2}|Chk0:0x{Check0:X2}|Chk1:0x{Check1:X2}|Chk02:0x{Check2:X2}]"; + } + } +} diff --git a/Mhf.Server/Packet/PacketId.cs b/Mhf.Server/Packet/PacketId.cs new file mode 100644 index 0000000..827021f --- /dev/null +++ b/Mhf.Server/Packet/PacketId.cs @@ -0,0 +1,438 @@ +namespace Mhf.Server.Packet +{ + public enum PacketId : ushort + { + MSG_HEAD = 0, + MSG_SYS_reserve01 = 1, + MSG_SYS_reserve02 = 2, + MSG_SYS_reserve03 = 3, + MSG_SYS_reserve04 = 4, + MSG_SYS_reserve05 = 5, + MSG_SYS_reserve06 = 6, + MSG_SYS_reserve07 = 7, + MSG_SYS_ADD_OBJECT = 8, + MSG_SYS_DEL_OBJECT = 9, + MSG_SYS_DISP_OBJECT = 10, + MSG_SYS_HIDE_OBJECT = 11, + MSG_SYS_reserve0C = 12, + MSG_SYS_reserve0D = 13, + MSG_SYS_reserve0E = 14, + MSG_SYS_EXTEND_THRESHOLD = 15, + MSG_SYS_END = 16, + MSG_SYS_NOP = 17, + MSG_SYS_ACK = 18, + MSG_SYS_TERMINAL_LOG = 19, + MSG_SYS_LOGIN = 20, + MSG_SYS_LOGOUT = 21, + MSG_SYS_SET_STATUS = 22, + MSG_SYS_PING = 23, + MSG_SYS_CAST_BINARY = 24, + MSG_SYS_HIDE_CLIENT = 25, + MSG_SYS_TIME = 26, + MSG_SYS_CASTED_BINARY = 27, + MSG_SYS_GET_FILE = 28, + MSG_SYS_ISSUE_LOGKEY = 29, + MSG_SYS_RECORD_LOG = 30, + MSG_SYS_ECHO = 31, + MSG_SYS_CREATE_STAGE = 32, + MSG_SYS_STAGE_DESTRUCT = 33, + MSG_SYS_ENTER_STAGE = 34, + MSG_SYS_BACK_STAGE = 35, + MSG_SYS_MOVE_STAGE = 36, + MSG_SYS_LEAVE_STAGE = 37, + MSG_SYS_LOCK_STAGE = 38, + MSG_SYS_UNLOCK_STAGE = 39, + MSG_SYS_RESERVE_STAGE = 40, + MSG_SYS_UNRESERVE_STAGE = 41, + MSG_SYS_SET_STAGE_PASS = 42, + MSG_SYS_WAIT_STAGE_BINARY = 43, + MSG_SYS_SET_STAGE_BINARY = 44, + MSG_SYS_GET_STAGE_BINARY = 45, + MSG_SYS_ENUMERATE_CLIENT = 46, + MSG_SYS_ENUMERATE_STAGE = 47, + MSG_SYS_CREATE_MUTEX = 48, + MSG_SYS_CREATE_OPEN_MUTEX = 49, + MSG_SYS_DELETE_MUTEX = 50, + MSG_SYS_OPEN_MUTEX = 51, + MSG_SYS_CLOSE_MUTEX = 52, + MSG_SYS_CREATE_SEMAPHORE = 53, + MSG_SYS_CREATE_ACQUIRE_SEMAPHORE = 54, + MSG_SYS_DELETE_SEMAPHORE = 55, + MSG_SYS_ACQUIRE_SEMAPHORE = 56, + MSG_SYS_RELEASE_SEMAPHORE = 57, + MSG_SYS_LOCK_GLOBAL_SEMA = 58, + MSG_SYS_UNLOCK_GLOBAL_SEMA = 59, + MSG_SYS_CHECK_SEMAPHORE = 60, + MSG_SYS_OPERATE_REGISTER = 61, + MSG_SYS_LOAD_REGISTER = 62, + MSG_SYS_NOTIFY_REGISTER = 63, + MSG_SYS_CREATE_OBJECT = 64, + MSG_SYS_DELETE_OBJECT = 65, + MSG_SYS_POSITION_OBJECT = 66, + MSG_SYS_ROTATE_OBJECT = 67, + MSG_SYS_DUPLICATE_OBJECT = 68, + MSG_SYS_SET_OBJECT_BINARY = 69, + MSG_SYS_GET_OBJECT_BINARY = 70, + MSG_SYS_GET_OBJECT_OWNER = 71, + MSG_SYS_UPDATE_OBJECT_BINARY = 72, + MSG_SYS_CLEANUP_OBJECT = 73, + MSG_SYS_reserve4A = 74, + MSG_SYS_reserve4B = 75, + MSG_SYS_reserve4C = 76, + MSG_SYS_reserve4D = 77, + MSG_SYS_reserve4E = 78, + MSG_SYS_reserve4F = 79, + MSG_SYS_INSERT_USER = 80, + MSG_SYS_DELETE_USER = 81, + MSG_SYS_SET_USER_BINARY = 82, + MSG_SYS_GET_USER_BINARY = 83, + MSG_SYS_NOTIFY_USER_BINARY = 84, + MSG_SYS_reserve55 = 85, + MSG_SYS_reserve56 = 86, + MSG_SYS_reserve57 = 87, + MSG_SYS_UPDATE_RIGHT = 88, + MSG_SYS_AUTH_QUERY = 89, + MSG_SYS_AUTH_DATA = 90, + MSG_SYS_AUTH_TERMINAL = 91, + MSG_SYS_reserve5C = 92, + MSG_SYS_RIGHTS_RELOAD = 93, + MSG_SYS_reserve5E = 94, + MSG_SYS_reserve5F = 95, + MSG_MHF_SAVEDATA = 96, + MSG_MHF_LOADDATA = 97, + MSG_MHF_LIST_MEMBER = 98, + MSG_MHF_OPR_MEMBER = 99, + MSG_MHF_ENUMERATE_DIST_ITEM = 100, + MSG_MHF_APPLY_DIST_ITEM = 101, + MSG_MHF_ACQUIRE_DIST_ITEM = 102, + MSG_MHF_GET_DIST_DESCRIPTION = 103, + MSG_MHF_SEND_MAIL = 104, + MSG_MHF_READ_MAIL = 105, + MSG_MHF_LIST_MAIL = 106, + MSG_MHF_OPRT_MAIL = 107, + MSG_MHF_LOAD_FAVORITE_QUEST = 108, + MSG_MHF_SAVE_FAVORITE_QUEST = 109, + MSG_MHF_REGISTER_EVENT = 110, + MSG_MHF_RELEASE_EVENT = 111, + MSG_MHF_TRANSIT_MESSAGE = 112, + MSG_SYS_reserve71 = 113, + MSG_SYS_reserve72 = 114, + MSG_SYS_reserve73 = 115, + MSG_SYS_reserve74 = 116, + MSG_SYS_reserve75 = 117, + MSG_SYS_reserve76 = 118, + MSG_SYS_reserve77 = 119, + MSG_SYS_reserve78 = 120, + MSG_SYS_reserve79 = 121, + MSG_SYS_reserve7A = 122, + MSG_SYS_reserve7B = 123, + MSG_SYS_reserve7C = 124, + MSG_CA_EXCHANGE_ITEM = 125, + MSG_SYS_reserve7E = 126, + MSG_MHF_PRESENT_BOX = 127, + MSG_MHF_SERVER_COMMAND = 128, + MSG_MHF_SHUT_CLIENT = 129, + MSG_MHF_ANNOUNCE = 130, + MSG_MHF_SET_LOGINWINDOW = 131, + MSG_SYS_TRANS_BINARY = 132, + MSG_SYS_COLLECT_BINARY = 133, + MSG_SYS_GET_STATE = 134, + MSG_SYS_SERIALIZE = 135, + MSG_SYS_ENUMLOBBY = 136, + MSG_SYS_ENUMUSER = 137, + MSG_SYS_INFOKYSERVER = 138, + MSG_MHF_GET_CA_UNIQUE_ID = 139, + MSG_MHF_SET_CA_ACHIEVEMENT = 140, + MSG_MHF_CARAVAN_MY_SCORE = 141, + MSG_MHF_CARAVAN_RANKING = 142, + MSG_MHF_CARAVAN_MY_RANK = 143, + MSG_MHF_CREATE_GUILD = 144, + MSG_MHF_OPERATE_GUILD = 145, + MSG_MHF_OPERATE_GUILD_MEMBER = 146, + MSG_MHF_INFO_GUILD = 147, + MSG_MHF_ENUMERATE_GUILD = 148, + MSG_MHF_UPDATE_GUILD = 149, + MSG_MHF_ARRANGE_GUILD_MEMBER = 150, + MSG_MHF_ENUMERATE_GUILD_MEMBER = 151, + MSG_MHF_ENUMERATE_CAMPAIGN = 152, + MSG_MHF_STATE_CAMPAIGN = 153, + MSG_MHF_APPLY_CAMPAIGN = 154, + MSG_MHF_ENUMERATE_ITEM = 155, + MSG_MHF_ACQUIRE_ITEM = 156, + MSG_MHF_TRANSFER_ITEM = 157, + MSG_MHF_MERCENARY_HUNTDATA = 158, + MSG_MHF_ENTRY_ROOKIE_GUILD = 159, + MSG_MHF_ENUMERATE_QUEST = 160, + MSG_MHF_ENUMERATE_EVENT = 161, + MSG_MHF_ENUMERATE_PRICE = 162, + MSG_MHF_ENUMERATE_RANKING = 163, + MSG_MHF_ENUMERATE_ORDER = 164, + MSG_MHF_ENUMERATE_SHOP = 165, + MSG_MHF_GET_EXTRA_INFO = 166, + MSG_MHF_UPDATE_INTERIOR = 167, + MSG_MHF_ENUMERATE_HOUSE = 168, + MSG_MHF_UPDATE_HOUSE = 169, + MSG_MHF_LOAD_HOUSE = 170, + MSG_MHF_OPERATE_WAREHOUSE = 171, + MSG_MHF_ENUMERATE_WAREHOUSE = 172, + MSG_MHF_UPDATE_WAREHOUSE = 173, + MSG_MHF_ACQUIRE_TITLE = 174, + MSG_MHF_ENUMERATE_TITLE = 175, + MSG_MHF_ENUMERATE_GUILD_ITEM = 176, + MSG_MHF_UPDATE_GUILD_ITEM = 177, + MSG_MHF_ENUMERATE_UNION_ITEM = 178, + MSG_MHF_UPDATE_UNION_ITEM = 179, + MSG_MHF_CREATE_JOINT = 180, + MSG_MHF_OPERATE_JOINT = 181, + MSG_MHF_INFO_JOINT = 182, + MSG_MHF_UPDATE_GUILD_ICON = 183, + MSG_MHF_INFO_FESTA = 184, + MSG_MHF_ENTRY_FESTA = 185, + MSG_MHF_CHARGE_FESTA = 186, + MSG_MHF_ACQUIRE_FESTA = 187, + MSG_MHF_STATE_FESTA_U = 188, + MSG_MHF_STATE_FESTA_G = 189, + MSG_MHF_ENUMERATE_FESTA_MEMBER = 190, + MSG_MHF_VOTE_FESTA = 191, + MSG_MHF_ACQUIRE_CAFE_ITEM = 192, + MSG_MHF_UPDATE_CAFEPOINT = 193, + MSG_MHF_CHECK_DAILY_CAFEPOINT = 194, + MSG_MHF_GET_COG_INFO = 195, + MSG_MHF_CHECK_MONTHLY_ITEM = 196, + MSG_MHF_ACQUIRE_MONTHLY_ITEM = 197, + MSG_MHF_CHECK_WEEKLY_STAMP = 198, + MSG_MHF_EXCHANGE_WEEKLY_STAMP = 199, + MSG_MHF_CREATE_MERCENARY = 200, + MSG_MHF_SAVE_MERCENARY = 201, + MSG_MHF_READ_MERCENARY_W = 202, + MSG_MHF_READ_MERCENARY_M = 203, + MSG_MHF_CONTRACT_MERCENARY = 204, + MSG_MHF_ENUMERATE_MERCENARY_LOG = 205, + MSG_MHF_ENUMERATE_GUACOT = 206, + MSG_MHF_UPDATE_GUACOT = 207, + MSG_MHF_INFO_TOURNAMENT = 208, + MSG_MHF_ENTRY_TOURNAMENT = 209, + MSG_MHF_ENTER_TOURNAMENT_QUEST = 210, + MSG_MHF_ACQUIRE_TOURNAMENT = 211, + MSG_MHF_GET_ACHIEVEMENT = 212, + MSG_MHF_RESET_ACHIEVEMENT = 213, + MSG_MHF_ADD_ACHIEVEMENT = 214, + MSG_MHF_PAYMENT_ACHIEVEMENT = 215, + MSG_MHF_DISPLAYED_ACHIEVEMENT = 216, + MSG_MHF_INFO_SCENARIO_COUNTER = 217, + MSG_MHF_SAVE_SCENARIO_DATA = 218, + MSG_MHF_LOAD_SCENARIO_DATA = 219, + MSG_MHF_GET_BBS_SNS_STATUS = 220, + MSG_MHF_APPLY_BBS_ARTICLE = 221, + MSG_MHF_GET_ETC_POINTS = 222, + MSG_MHF_UPDATE_ETC_POINT = 223, + MSG_MHF_GET_MYHOUSE_INFO = 224, + MSG_MHF_UPDATE_MYHOUSE_INFO = 225, + MSG_MHF_GET_WEEKLY_SCHEDULE = 226, + MSG_MHF_ENUMERATE_INV_GUILD = 227, + MSG_MHF_OPERATION_INV_GUILD = 228, + MSG_MHF_STAMPCARD_STAMP = 229, + MSG_MHF_STAMPCARD_PRIZE = 230, + MSG_MHF_UNRESERVE_SRG = 231, + MSG_MHF_LOAD_PLATE_DATA = 232, + MSG_MHF_SAVE_PLATE_DATA = 233, + MSG_MHF_LOAD_PLATE_BOX = 234, + MSG_MHF_SAVE_PLATE_BOX = 235, + MSG_MHF_READ_GUILDCARD = 236, + MSG_MHF_UPDATE_GUILDCARD = 237, + MSG_MHF_READ_BEAT_LEVEL = 238, + MSG_MHF_UPDATE_BEAT_LEVEL = 239, + MSG_MHF_READ_BEAT_LEVEL_ALL_RANKING = 240, + MSG_MHF_READ_BEAT_LEVEL_MY_RANKING = 241, + MSG_MHF_READ_LAST_WEEK_BEAT_RANKING = 242, + MSG_MHF_ACCEPT_READ_REWARD = 243, + MSG_MHF_GET_ADDITIONAL_BEAT_REWARD = 244, + MSG_MHF_GET_FIXED_SEIBATU_RANKING_TABLE = 245, + MSG_MHF_GET_BBS_USER_STATUS = 246, + MSG_MHF_KICK_EXPORT_FORCE = 247, + MSG_MHF_GET_BREAK_SEIBATU_LEVEL_REWARD = 248, + MSG_MHF_GET_WEEKLY_SEIBATU_RANKING_REWARD = 249, + MSG_MHF_GET_EARTH_STATUS = 250, + MSG_MHF_LOAD_PARTNER = 251, + MSG_MHF_SAVE_PARTNER = 252, + MSG_MHF_GET_GUILD_MISSION_LIST = 253, + MSG_MHF_GET_GUILD_MISSION_RECORD = 254, + MSG_MHF_ADD_GUILD_MISSION_COUNT = 255, + MSG_MHF_SET_GUILD_MISSION_TARGET = 256, + MSG_MHF_CANCEL_GUILD_MISSION_TARGET = 257, + MSG_MHF_LOAD_OTOMO_AIROU = 258, + MSG_MHF_SAVE_OTOMO_AIROU = 259, + MSG_MHF_ENUMERATE_GUILD_TRESURE = 260, + MSG_MHF_ENUMERATE_AIROULIST = 261, + MSG_MHF_REGIST_GUILD_TRESURE = 262, + MSG_MHF_ACQUIRE_GUILD_TRESURE = 263, + MSG_MHF_OPERATE_GUILD_TRESURE_REPORT = 264, + MSG_MHF_GET_GUILD_TRESURE_SOUVENIR = 265, + MSG_MHF_ACQUIRE_GUILD_TRESURE_SOUVENIR = 266, + MSG_MHF_ENUMERATE_FESTA_INTERMEDIATE_PRIZE = 267, + MSG_MHF_ACQUIRE_FESTA_INTERMEDIATE_PRIZE = 268, + MSG_MHF_LOAD_DECO_MYSET = 269, + MSG_MHF_SAVE_DECO_MYSET = 270, + MSG_MHF_reserve010F = 271, + MSG_MHF_LOAD_GUILD_COOKING = 272, + MSG_MHF_REGIST_GUILD_COOKING = 273, + MSG_MHF_LOAD_GUILD_ADVENTURE = 274, + MSG_MHF_REGIST_GUILD_ADVENTURE = 275, + MSG_MHF_ACQUIRE_GUILD_ADVENTURE = 276, + MSG_MHF_CHARGE_GUILD_ADVENTURE = 277, + MSG_MHF_LOAD_LEGEND_DISPATCH = 278, + MSG_MHF_LOAD_HUNTER_NAVI = 279, + MSG_MHF_SAVE_HUNTER_NAVI = 280, + MSG_MHF_REGIST_SPABI_TIME = 281, + MSG_MHF_GET_GUILD_WEEKLY_BONUS_MASTER = 282, + MSG_MHF_GET_GUILD_WEEKLY_BONUS_ACTIVE_COUNT = 283, + MSG_MHF_ADD_GUILD_WEEKLY_BONUS_EXCEPTIONAL_USER = 284, + MSG_MHF_GET_TOWER_INFO = 285, + MSG_MHF_POST_TOWER_INFO = 286, + MSG_MHF_GET_GEM_INFO = 287, + MSG_MHF_POST_GEM_INFO = 288, + MSG_MHF_GET_EARTH_VALUE = 289, + MSG_MHF_DEBUG_POST_VALUE = 290, + MSG_MHF_GET_PAPER_DATA = 291, + MSG_MHF_GET_NOTICE = 292, + MSG_MHF_POST_NOTICE = 293, + MSG_MHF_GET_BOOST_TIME = 294, + MSG_MHF_POST_BOOST_TIME = 295, + MSG_MHF_GET_BOOST_TIME_LIMIT = 296, + MSG_MHF_POST_BOOST_TIME_LIMIT = 297, + MSG_MHF_ENUMERATE_FESTA_PERSONAL_PRIZE = 298, + MSG_MHF_ACQUIRE_FESTA_PERSONAL_PRIZE = 299, + MSG_MHF_GET_RAND_FROM_TABLE = 300, + MSG_MHF_GET_CAFE_DURATION = 301, + MSG_MHF_GET_CAFE_DURATION_BONUS_INFO = 302, + MSG_MHF_RECEIVE_CAFE_DURATION_BONUS = 303, + MSG_MHF_POST_CAFE_DURATION_BONUS_RECEIVED = 304, + MSG_MHF_GET_GACHA_POINT = 305, + MSG_MHF_USE_GACHA_POINT = 306, + MSG_MHF_EXCHANGE_FPOINT_2_ITEM = 307, + MSG_MHF_EXCHANGE_ITEM_2_FPOINT = 308, + MSG_MHF_GET_FPOINT_EXCHANGE_LIST = 309, + MSG_MHF_PLAY_STEPUP_GACHA = 310, + MSG_MHF_RECEIVE_GACHA_ITEM = 311, + MSG_MHF_GET_STEPUP_STATUS = 312, + MSG_MHF_PLAY_FREE_GACHA = 313, + MSG_MHF_GET_TINY_BIN = 314, + MSG_MHF_POST_TINY_BIN = 315, + MSG_MHF_GET_SENYU_DAILY_COUNT = 316, + MSG_MHF_GET_GUILD_TARGET_MEMBER_NUM = 317, + MSG_MHF_GET_BOOST_RIGHT = 318, + MSG_MHF_START_BOOST_TIME = 319, + MSG_MHF_POST_BOOST_TIME_QUEST_RETURN = 320, + MSG_MHF_GET_BOX_GACHA_INFO = 321, + MSG_MHF_PLAY_BOX_GACHA = 322, + MSG_MHF_RESET_BOX_GACHA_INFO = 323, + MSG_MHF_GET_SEIBATTLE = 324, + MSG_MHF_POST_SEIBATTLE = 325, + MSG_MHF_GET_RYOUDAMA = 326, + MSG_MHF_POST_RYOUDAMA = 327, + MSG_MHF_GET_TENROUIRAI = 328, + MSG_MHF_POST_TENROUIRAI = 329, + MSG_MHF_POST_GUILD_SCOUT = 330, + MSG_MHF_CANCEL_GUILD_SCOUT = 331, + MSG_MHF_ANSWER_GUILD_SCOUT = 332, + MSG_MHF_GET_GUILD_SCOUT_LIST = 333, + MSG_MHF_GET_GUILD_MANAGE_RIGHT = 334, + MSG_MHF_SET_GUILD_MANAGE_RIGHT = 335, + MSG_MHF_PLAY_NORMAL_GACHA = 336, + MSG_MHF_GET_DAILY_MISSION_MASTER = 337, + MSG_MHF_GET_DAILY_MISSION_PERSONAL = 338, + MSG_MHF_SET_DAILY_MISSION_PERSONAL = 339, + MSG_MHF_GET_GACHA_PLAY_HISTORY = 340, + MSG_MHF_GET_REJECT_GUILD_SCOUT = 341, + MSG_MHF_SET_REJECT_GUILD_SCOUT = 342, + MSG_MHF_GET_CA_ACHIEVEMENT_HIST = 343, + MSG_MHF_SET_CA_ACHIEVEMENT_HIST = 344, + MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS = 345, + MSG_MHF_USE_KEEP_LOGIN_BOOST = 346, + MSG_MHF_GET_UD_SCHEDULE = 347, + MSG_MHF_GET_UD_INFO = 348, + MSG_MHF_GET_KIJU_INFO = 349, + MSG_MHF_SET_KIJU = 350, + MSG_MHF_ADD_UD_POINT = 351, + MSG_MHF_GET_UD_MY_POINT = 352, + MSG_MHF_GET_UD_TOTAL_POINT_INFO = 353, + MSG_MHF_GET_UD_BONUS_QUEST_INFO = 354, + MSG_MHF_GET_UD_SELECTED_COLOR_INFO = 355, + MSG_MHF_GET_UD_MONSTER_POINT = 356, + MSG_MHF_GET_UD_DAILY_PRESENT_LIST = 357, + MSG_MHF_GET_UD_NORMA_PRESENT_LIST = 358, + MSG_MHF_GET_UD_RANKING_REWARD_LIST = 359, + MSG_MHF_ACQUIRE_UD_ITEM = 360, + MSG_MHF_GET_REWARD_SONG = 361, + MSG_MHF_USE_REWARD_SONG = 362, + MSG_MHF_ADD_REWARD_SONG_COUNT = 363, + MSG_MHF_GET_UD_RANKING = 364, + MSG_MHF_GET_UD_MY_RANKING = 365, + MSG_MHF_ACQUIRE_MONTHLY_REWARD = 366, + MSG_MHF_GET_UD_GUILD_MAP_INFO = 367, + MSG_MHF_GENERATE_UD_GUILD_MAP = 368, + MSG_MHF_GET_UD_TACTICS_POINT = 369, + MSG_MHF_ADD_UD_TACTICS_POINT = 370, + MSG_MHF_GET_UD_TACTICS_RANKING = 371, + MSG_MHF_GET_UD_TACTICS_REWARD_LIST = 372, + MSG_MHF_GET_UD_TACTICS_LOG = 373, + MSG_MHF_GET_EQUIP_SKIN_HIST = 374, + MSG_MHF_UPDATE_EQUIP_SKIN_HIST = 375, + MSG_MHF_GET_UD_TACTICS_FOLLOWER = 376, + MSG_MHF_SET_UD_TACTICS_FOLLOWER = 377, + MSG_MHF_GET_UD_SHOP_COIN = 378, + MSG_MHF_USE_UD_SHOP_COIN = 379, + MSG_MHF_GET_ENHANCED_MINIDATA = 380, + MSG_MHF_SET_ENHANCED_MINIDATA = 381, + MSG_MHF_SEX_CHANGER = 382, + MSG_MHF_GET_LOBBY_CROWD = 383, + MSG_SYS_reserve180 = 384, + MSG_MHF_GUILD_HUNTDATA = 385, + MSG_MHF_ADD_KOURYOU_POINT = 386, + MSG_MHF_GET_KOURYOU_POINT = 387, + MSG_MHF_EXCHANGE_KOURYOU_POINT = 388, + MSG_MHF_GET_UD_TACTICS_BONUS_QUEST = 389, + MSG_MHF_GET_UD_TACTICS_FIRST_QUEST_BONUS = 390, + MSG_MHF_GET_UD_TACTICS_REMAINING_POINT = 391, + MSG_SYS_reserve188 = 392, + MSG_MHF_LOAD_PLATE_MYSET = 393, + MSG_MHF_SAVE_PLATE_MYSET = 394, + MSG_SYS_reserve18B = 395, + MSG_MHF_GET_RESTRICTION_EVENT = 396, + MSG_MHF_SET_RESTRICTION_EVENT = 397, + MSG_SYS_reserve18E = 398, + MSG_SYS_reserve18F = 399, + MSG_MHF_GET_TREND_WEAPON = 400, + MSG_MHF_UPDATE_USE_TREND_WEAPON_LOG = 401, + MSG_SYS_reserve192 = 402, + MSG_SYS_reserve193 = 403, + MSG_SYS_reserve194 = 404, + MSG_MHF_SAVE_RENGOKU_DATA = 405, + MSG_MHF_LOAD_RENGOKU_DATA = 406, + MSG_MHF_GET_RENGOKU_BINARY = 407, + MSG_MHF_ENUMERATE_RENGOKU_RANKING = 408, + MSG_MHF_GET_RENGOKU_RANKING_RANK = 409, + MSG_MHF_ACQUIRE_EXCHANGE_SHOP = 410, + MSG_SYS_reserve19B = 411, + MSG_MHF_SAVE_MEZFES_DATA = 412, + MSG_MHF_LOAD_MEZFES_DATA = 413, + MSG_SYS_reserve19E = 414, + MSG_SYS_reserve19F = 415, + MSG_MHF_UPDATE_FORCE_GUILD_RANK = 416, + MSG_MHF_RESET_TITLE = 417, + MSG_SYS_reserve202 = 418, + MSG_SYS_reserve203 = 419, + MSG_SYS_reserve204 = 420, + MSG_SYS_reserve205 = 421, + MSG_SYS_reserve206 = 422, + MSG_SYS_reserve207 = 423, + MSG_SYS_reserve208 = 424, + MSG_SYS_reserve209 = 425, + MSG_SYS_reserve20A = 426, + MSG_SYS_reserve20B = 427, + MSG_SYS_reserve20C = 428, + MSG_SYS_reserve20D = 429, + MSG_SYS_reserve20E = 430, + MSG_SYS_reserve20F = 431, + } +} diff --git a/Mhf.Server/Packet/PacketResponse.cs b/Mhf.Server/Packet/PacketResponse.cs new file mode 100644 index 0000000..e1f7596 --- /dev/null +++ b/Mhf.Server/Packet/PacketResponse.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Arrowgene.Services.Buffers; +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public abstract class PacketResponse + { + private readonly List _receiver; + private MhfPacket _packet; + + public PacketResponse(ushort id) + { + _receiver = new List(); + Id = id; + } + + public List Receiver => new List(_receiver); + public ushort Id { get; } + + protected abstract IBuffer ToBuffer(); + + public MhfPacket ToPacket() + { + if (_packet == null) + { + _packet = new MhfPacket(Id, ToBuffer()); + } + + return _packet; + } + + public void AddReceiver(params ISender[] receiver) + { + _receiver.AddRange(receiver); + } + + public void AddReceiver(IEnumerable receiver) + { + _receiver.AddRange(receiver); + } + + public void CleatReceivers() + { + _receiver.Clear(); + } + } +} diff --git a/Mhf.Server/Packet/PacketRouter.cs b/Mhf.Server/Packet/PacketRouter.cs new file mode 100644 index 0000000..418f81d --- /dev/null +++ b/Mhf.Server/Packet/PacketRouter.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Arrowgene.Services.Buffers; +using Arrowgene.Services.Logging; +using Mhf.Server.Logging; +using Mhf.Server.Model; + +namespace Mhf.Server.Packet +{ + public class PacketRouter + { + private readonly MhfLogger _logger; + + public PacketRouter() + { + _logger = LogProvider.Logger(this); + } + + /// + /// Send a packet to a client. + /// + public void Send(ISender client, MhfPacket packet) + { + client.Send(packet); + } + + /// + /// Send a packet to a client. + /// + public void Send(ISender client, ushort id, IBuffer data) + { + MhfPacket packet = new MhfPacket(id, data); + Send(client, packet); + } + + /// + /// Send a packet to multiple clients. + /// + /// clients to exclude + public void Send(List clients, MhfPacket packet, params ISender[] excepts) + { + clients = GetClients(clients, excepts); + foreach (MhfClient client in clients) + { + Send(client, packet); + } + } + + /// + /// Send a packet to multiple clients. + /// + /// clients to exclude + public void Send(List clients, ushort id, IBuffer data, params ISender[] excepts) + { + Send(clients, new MhfPacket(id, data), excepts); + } + + /// + /// Send a specific packet response. + /// + public void Send(PacketResponse response) + { + foreach (ISender client in response.Receiver) + { + Send(client, response.ToPacket()); + } + } + + /// + /// Send a specific packet response. + /// + public void Send(PacketResponse response, params ISender[] clients) + { + response.AddReceiver(clients); + Send(response); + } + + private List GetClients(List clients, params ISender[] excepts) + { + if (excepts.Length == 0) + { + return clients; + } + + foreach (ISender except in excepts) + { + clients.Remove(except); + } + + return clients; + } + } +} diff --git a/Mhf.Server/PacketHandler/MsgHeadHandler.cs b/Mhf.Server/PacketHandler/MsgHeadHandler.cs new file mode 100644 index 0000000..2dc2eaa --- /dev/null +++ b/Mhf.Server/PacketHandler/MsgHeadHandler.cs @@ -0,0 +1,27 @@ +using Mhf.Server.Model; +using Mhf.Server.Packet; +using Mhf.Server.PacketResponses; + +namespace Mhf.Server.PacketHandler +{ + public class MsgHeadHandler : ConnectionHandler + { + public MsgHeadHandler(MhfServer server) : base(server) + { + } + + public override ushort Id => (ushort) PacketId.MSG_HEAD; + + public override void Handle(MhfConnection connection, MhfPacket packet) + { + string keySign = packet.Data.ReadCString(); + string userId = packet.Data.ReadCString(); + string sessionKey = packet.Data.ReadCString(); + + Logger.Info($"Auth Request: KeySign:{keySign} UserId:{userId} SessionKey:{sessionKey}"); + + MsgHeadResponse response = new MsgHeadResponse(); + Router.Send(response, connection); + } + }; +} diff --git a/Mhf.Server/PacketResponses/MsgHeadResponse.cs b/Mhf.Server/PacketResponses/MsgHeadResponse.cs new file mode 100644 index 0000000..336e375 --- /dev/null +++ b/Mhf.Server/PacketResponses/MsgHeadResponse.cs @@ -0,0 +1,42 @@ +using Arrowgene.Services.Buffers; +using Mhf.Server.Common; +using Mhf.Server.Packet; + +namespace Mhf.Server.PacketResponses +{ + public class MsgHeadResponse : PacketResponse + { + public MsgHeadResponse() : base((ushort) PacketId.MSG_HEAD) + { + } + + protected override IBuffer ToBuffer() + { + string fileChecksumHost = "l0.mhf-g.jp"; + string fileHost = "u0.mhf-g.jp"; + string lobbyConnection = "127.0.0.1:53310"; + + IBuffer res = BufferProvider.Provide(); + res.SetPositionStart(); + res.WriteByte(1); //unk_hostname_count + res.WriteByte(2); //entrance_server_count + res.WriteByte(1); //character_count + res.WriteByte(1); //character_count + res.WriteBytes(new byte[] + { + 0x13, 0x50, 0xDE, 0xED, 0x44, 0x6C, 0x58, 0x46, 0x7A, 0x77, 0x73, 0x75, 0x42, 0x30, 0x41, 0x45, 0x34, + 0x75, 0x77, 0x41, 0x5D, 0xCF, 0x28, 0x85 + }); + res.WriteByte((byte) (fileHost.Length + 1)); //str len + 00 + res.WriteCString(fileHost); + res.WriteByte((byte) (fileChecksumHost.Length + 1)); //str len + 00 + res.WriteCString(fileChecksumHost); + res.WriteByte((byte) (lobbyConnection.Length + 1)); //str len + 00 + res.WriteCString(lobbyConnection); + string d = + "2A14BEE7000100075DCC8E68000000017370656E676500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000002643C424F44593E3C43454E5445523E3C53495A455F333E3C435F343E8CC397B48EED82AA82A893BE81493C42523E3C424F44593E817591E6333989F189CC95508EEB897190ED8145905E90E081768A4A8DC33C42523E3C424F44593E3C42523E3C424F44593E3C4C4546543E3C435F333E3C53495A455F323E817990ED89CC82CC8FCD8A4A8DC38AFA8AD4817A3C435F373E3C42523E3C424F44593E31318C8E313393FA289085292081602031318C8E323093FA289085298381839383658369839383588A4A8E6E82DC82C53C42523E3C424F44593E3C42523E3C424F44593E3C42523E3C424F44593E3C435F343E81A18CC397B48EED82AA82A893BE81493C435F373E3C42523E3C424F44593E3C53495A455F323E8CC397B48EED82CC838283938358835E815B82F08EEB97C282B582C48A6C93BE82C582AB82E98C7D8C82837C83438393836782F0919D97CA81423C42523E3C424F44593E3C42523E3C424F44593E3C435F343E81A1834E8347835883678F89834E838A8341837B815B8369835882F0919D97CA81493C435F373E3C42523E3C424F44593E90ED89CC82CC8FCD82C58A6C93BE82C582AB82E9834E8347835883678F89834E838A8341837B815B8369835882AA3C42523E3C424F44593E92CA8FED82E682E882E03C435F353E353030837C8343839383678341836283768149203C435F373E3C42523E3C424F44593E3C42523E3C424F44593E91BD82AD82CC8EED97DE82CC8C7D8C8290ED834E83478358836782F0834E838A834182B581413C42523E3C424F44593E8F89834E838A8341837B815B8369835882F082BD82AD82B382F18A6C93BE82B582E682A48149002A14BEE70000000E2A64736D6300080C00003D000000818100000000000029000000816A0000000000002800000081690000000000002100000081490000000000002F000000815E0000000000002B000000817B00000000000026000000819500000000000082500000310000000000000082DA0000837B0000CE00DE0082D9DE00837ADE0082D9814A837A814ACE00814A0000000082D7000083780000CD00DE0082D6DE008377DE0082D6814ACD00814A8377814A0000000082C5000083660000C300DE0082C4DE008365DE0082C4814A8365814AC300814A81A7814A81A7DE0089B3DE0089B3814A0000000082D1000083720000CB00DE0082D0DE008371DE0082D0814A8371814ACB00814A0000000082C7000083680000C400DE0082C6DE008367DE0082C6814A8367814AC400814A84B0DE0084B0814A84A5DE0084A5814A0000000082CE0000836F0000CA00DE0082CDDE00836EDE0094AADE0082CD814A836E814ACA00814A94AA814A0000000082C2DE00836400008363DE0082C2814A8363814AC200DE00C200814A82C3000082C1DE008362DE00AF00DE0082C1814A8362814AAF00814A0000000082D4000083750000CC00DE0083940000B300DE0082A4814A82A4DE008345DE00A900DE0082A3DE0082D3DE008374DE00CC00814A0000000082C0000083610000C100DE0082BFDE008360DE0082BF814A8360814AC100814A90E7814A90E7DE000000000082BE0000835F0000C000DE0082BDDE00835EDE00975BDE0082BD814A835E814AC000814A975B814A0000000082BC0000835D0000BF00DE0082BBDE00835CDE0082BB814A835C814ABF00814A8393DE008393814ADD00814ADD00DE00838ADE00D800DE00D800814A838A814A0000000082BA0000BE00DE0082B9DE00835ADE0082B9814A835A814ABE00814A835B00000000000082B8000083590000BD00DE0082B7DE008358DE0082B7814A8358814ABD00814A0000000082B6000083570000BC00DE0082B5DE008356DE0082B5814A8356814ABC00814A0000000082B4000083550000BB00DE0082B3DE008354DE0082B3814A8354814ABB00814A0000000082B2000083530000BA00DE0082B1DE008352DE0082B1814A8352814ABA00814A0000000082B0000083510000B900DE0082AFDE008350DE0082AF814A8350814AB900814A8396DE008396814A0000000082AE0000834F0000B800DE0082ADDE00834EDE0082AD814A834E814AB800814A0000000082AC0000834D0000B700DE0082ABDE00834CDE0082AB814A834C814AB700814A0000000082AA0000834B0000B600DE008395DE00834ADE0082A9DE0097CDDE008395814A834A814A82A9814A97CD814AB600814A0000000082F0000083920000A60000000000000082ED0000838F0000DC000000838E00000000000082EB0000838D0000DB00000081A000008CFB00000000000082EA0000838C0000DA0000000000000082E90000838B0000D90000000000000082E80000838A0000D80000000000000082E7000083890000D70000000000000082E6000083880000D6000000AE00000082E50000838700000000000082E4000083860000D5000000AD00000082E30000838500000000000082E2000083840000D4000000AC00000082E10000838300000000000082E0000083820000D30000000000000082DF000083810000D20000004D0045000000000082DE000083800000D10000000000000082DD0000837E0000D00000000000000082DC0000837D0000CF0000000000000082D90000837A0000CE0000000000000082D6000083770000CD0000000000000082D3000083740000CC0000000000000082D0000083710000CB0000000000000082CD0000836E0000CA00000094AA00000000000082CC0000836D0000C90000000000000082CB0000836C0000C80000000000000082CA0000836B0000C70000000000000082C90000836A0000C600000093F100000000000082C8000083690000C50000000000000082C6000083670000C400000084B0000084A500000000000082C4000083650000C300000081A7000089B300000000000082C2000083630000C200000082C1000083620000AF0000000000000082BF000083600000C100000090E700000000000082BD0000835E0000C0000000975B00000000000082BB0000835C0000BF0000000000000082B90000835A0000BE0000000000000082B7000083580000BD0000000000000082B5000083560000BC0000000000000082B3000083540000BB0000000000000082B1000083520000BA0000000000000082AF000083500000B9000000839600000000000082AD0000834E0000B80000000000000082AB0000834C0000B70000000000000082A90000834A0000B60000008395000097CD00000000000082A8000083490000B5000000AB00000082A70000834800000000000082A6000083470000B4000000AA00000082A50000834600008D4800000000000082A4000083450000B3000000A900000082A30000834400000000000082A2000083430000B2000000A800000082A100008342000000000000A7000000B1000000829F000082A00000834000008341000000000000815B0000815C0000815D0000816000002D000000817C0000B000000088EA00000000000039000000825800000000000038000000825700000000000037000000825600000000000036000000825500000000000035000000825400000000000034000000825300000000000033000000825200000000000032000000825100000000000082DB0000837C0000CE00DF0082D9DF00837ADF00837A818B82D9818BCE00818B0000000082D8000083790000CD00DF0082D6DF008377DF008377818B82D6818BCD00818B0000000082D5000083760000CC00DF0082D3DF008374DF008374818B82D3818BCC00818B0000000082D2000083730000CB00DF0082D0DF008371DF008371818B82D0818BCB00818B0000000082CF000083700000CA00DF0082CDDF00836EDF00836E818B82CD818BCA00818B94AADF0094AA814B0000000082790000829A00005A0000007A00000083A40000000000008278000082990000590000007900000083B200008454000084850000000000008277000082980000580000007800000083B4000083D4000084560000817E000084870000875D0000000000008276000082970000570000007700000083D6000084590000848A0000848B0000000000008275000082960000560000007600000083CB000083D2000087580000000000008274000082950000550000007500000083CA000081BE0000000000008273000082940000540000007400000083B1000083D100008453000084840000000000002400000053000000730000008190000081E70000827200008293000000000000827100008292000052000000720000008460000084910000000000008270000082910000510000007100000000000000826F000082900000500000007000000083AF000083CF0000845100008482000000000000826E0000828F00004F0000006F000000819B000083AD000083CD0000844F00008480000081FC0000815A000030000000824F000000000000826D0000828E00004E0000006E00000083AB000083C50000DD00000082F100008393000000000000826C0000828D00004D0000006D00000083AA0000844D0000847D000000000000826B0000828C00004C0000006C000000879800007C00000000000000826A0000828B00004B0000006B00000083A8000083C80000844B0000847B00000000000082690000828A00004A0000006A000000000000008268000082890000490000006900000083A7000087540000000000008267000082880000480000006800000083A50000844E0000847E000000000000826500008286000046000000660000000000000082660000828700004700000067000000000000008264000082850000450000006500000083A3000083C300008445000084460000847500008476000081B80000000000008263000082840000440000006400000000000000430000008283000063000000845200008483000082620000818E0000000000004200000082610000828200006200000083C0000083A000008442000084720000848C0000848E000081F30000000000002700000081660000000000004100000082600000610000008281000083BF000040000000819700008470000081F0000084400000839F000000000000220000008168000000000000250000008193000000000000000000006E616D002816000002000000937AFFFF97EAFFFF0000FFFF0400000082B78B0182A9A80182C6660182EBFC000000FFFF020000009273FFFF8F97FFFF0000FFFF0300000082A2C401826D870282DBF3010000FFFF050000008266C2024100EF02826D87028269A9024100EF020000FFFF0400000082C46C0182A2C401826D870282B197010000FFFF0300000082DC380182BB830182B197010000FFFF0300000082BF790182BB830182B197010000FFFF020000008BADFFFF9390FFFF0000FFFF0400000082CF170282A2C40182CF1702826D87020000FFFF03000000965CFFFF82A9A8019263FFFF0000FFFF0400000082B9870182ADA00182EBFC0082B78B010000FFFF0400000082CE4D0082BD7E01815BD2018CA2FFFF0000FFFF0500000082DC380182E80A0182D34401A700CB0182C862010000FFFF0400000082D5050282C2720182B58F01815BD2010000FFFF0400000082B1970182A9A80182A2C401826D87020000FFFF0400000082C46C0182A2C401826D870282DBF3010000FFFF0400000082D13700815BD20182BF790182ADA0010000FFFF0200000090ABFFFF977EFFFF0000FFFF0300000082C8620182BF790182B78B010000FFFF0400000082EBFC0082E80A0182B19701826D87020000FFFF0400000082C46C0182A2C40182DE300182DBF3010000FFFF0400000082C52A0082A9A80182DC380182E70E010000FFFF0400000082CF170282A2C40182B8A50082E80A010000FFFF0400000082DC3801826D870282A9A80182B78B010000FFFF0D0000004100EF028263D302826C91028268AE02826D87028268AE0224005C0282735302827164024100EF0282735302826E7902827164020000FFFF0600000082A8AE018B71FFFF976CFFFF918AFFFF926BFFFF8EBAFFFF0000FFFF0200000096F2FFFF95A8FFFF0000FFFF0400000092D2FFFF967BFFFF8C9BFFFF8E4FFFFF0000FFFF0400000092D2FFFF967BFFFF90B0FFFF8D4FFFFF0000FFFF040000008FACFFFF96ECFFFF8B60FFFF93BFFFFF0000FFFF0300000082716402826C9102827353020000FFFF040000009099FFFF8959FFFF815BD20193BFFFFF0000FFFF0400000092D2FFFF967BFFFF97C7FFFF8E4FFFFF0000FFFF0300000093A1FFFF89AAFFFF9776FFFF0000FFFF04000000815BD20190A3FFFF91D7FFFF94CDFFFF0000FFFF050000008FACFFFF9388FFFF9054FFFF91BEFFFF9859FFFF0000FFFF0200000091E5FFFF9683FFFF0000FFFF030000008A6FFFFF90C1FFFF8DDCFFFF0000FFFF020000009FAFFFFF92B0FFFF0000FFFF020000009683FFFF96F2FFFF0000FFFF0300000092AAFFFF9081FFFF82ABA4010000FFFF0300000082A9A80182D505029859FFFF0000FFFF010000008179FFFF0000FFFF01000000817AFFFF0000FFFF020000008941FFFF8C73FFFF0000FFFF020000009675FFFF8B4EFFFF0000FFFF0200000089BAFFFF979FFFFF0000FFFF0600000082A8AE0182A4BD0182DE3001905EFFFF979DFFFF8BB3FFFF0000FFFF090000008269A9024100EF02826F7002826AA0028268AE02826B9902826B99028264C702827164020000FFFF030000008B43FFFF88E1FFFF82A2C4010000FFFF040000008B43FFFF82BF790182AAE60082A2C4010000FFFF0400000082ABA40182BF790182AAE60082A2C4010000FFFF030000008B43FFFF8BB6FFFF82A2C4010000FFFF0400000090B8FFFF905FFFFF9496FFFF8EE3FFFF0000FFFF04000000926DFFFF8C62FFFF9278FFFF82EA02010000FFFF0500000082BF790182A6B50182A8AE0182ADA00182EA02010000FFFF0200000096DAFFFF88C3FFFF0000FFFF0200000096BEFFFF96D3FFFF0000FFFF05000000A700CB0182ABA40182DF2B0182ADA00182E70E010000FFFF0400000082EBFC00826D870282CF170282E80A010000FFFF02000000E19AFFFF9561FFFF0000FFFF0300000082E70E0182A2C4019561FFFF0000FFFF020000009492FFFF9273FFFF0000FFFF01000000E6EAFFFF0000FFFF0300000082BF7901826D870282CE4D000000FFFF0100000098FBFFFF0000FFFF0200000098FBFFFF9943FFFF0000FFFF02000000936AFFFF8E45FFFF0000FFFF020000008CEEFFFF9048FFFF0000FFFF02000000E6CBFFFF96AFFFFF0000FFFF02000000E271FFFF91BDFFFF0000FFFF0200000094F1FFFF906CFFFF0000FFFF030000009056FFFF95BDFFFF96AFFFFF0000FFFF0400000093C1FFFF8EEAFFFF9594FFFF978EFFFF0000FFFF0500000082CE4D0082A9A80182BF790182E61201826D87020000FFFF0400000082BF790182E61201826D87028CF6FFFF0000FFFF0500000082C6660182E9060182B197019597FFFF9843FFFF0000FFFF0400000082C6660182E9060182B197018FECFFFF0000FFFF03000000A700CB0182C8620182E906010000FFFF0300000082A4BD01826D870282B197010000FFFF0200000096D1FFFF9382FFFF0000FFFF030000008D95FFFF826D870282DA18000000FFFF040000009573FFFF89C2FFFF9047FFFF96AFFFFF0000FFFF020000009484FFFF8F97FFFF0000FFFF0200000088FAFFFF9484FFFF0000FFFF020000008BADFFFF8AADFFFF0000FFFF0400000082E2200182E80A0182DC3801826D87020000FFFF0300000082DC3801826D870282B197010000FFFF0300000082A8AE0182DF2B0182B197010000FFFF0300000082BF7901826D870282B197010000FFFF0300000082BF7901826D870282DBF3010000FFFF0300000082BF7901826D870282DA18000000FFFF0400000082B9870182C2720182ADA00182B78B010000FFFF0300000024005C028264C702827730020000FFFF020000009484FFFF8F74FFFF0000FFFF020000009483FFFF8F74FFFF0000FFFF0300000082EA020182A2C40182D505020000FFFF0400000082B78B0182D8FC0182E9060182DC38010000FFFF0500000082ADA00182E80A0182C6660182E80A0182B78B010000FFFF0500000082D3440182A6B50182E70E0182BF790182A8AE010000FFFF0300000082B4B700815BD20196CBFFFF0000FFFF0400000082B4B700815BD20182DF2B01826D87020000FFFF0400000082B98701826D870282B8A50082E80A010000FFFF0400000082A8AE0182C8620182C95D01815BD2010000FFFF030000008FE1FFFF8A51FFFF8ED2FFFF0000FFFF040000008265BD0282744C024300D802826AA0020000FFFF0400000082D34401A700CB0182C2720182ADA0010000FFFF0500000082BF790182E22001826D870282B1970182EBFC000000FFFF050000004200E0028268AE02827353024300D8028267B5020000FFFF040000004300D80282744C02826D8702827353020000FFFF05000000826F70028264C702826D87028268AE0224005C020000FFFF0300000082D8FC0182C95D0182B78B010000FFFF06000000827544024100EF028266C2028268AE02826D87024100EF020000FFFF0300000082CE4D0082ACDD0082C862010000FFFF0400000082D46700A700CB0182ACDD0082C862010000FFFF05000000826F700282744C0224005C0224005C02827828020000FFFF0200000090B8FFFF8974FFFF0000FFFF020000008E80FFFF82CB55010000FFFF020000008E45FFFF82B78B010000FFFF0100000095B3FFFF0000FFFF020000008998FFFF95A8FFFF0000FFFF0400000082BF7901826D870282DBF30182B197010000FFFF0400000082DBF30182B1970182BF7901826D87020000FFFF0300000082B3930182B987018E71FFFF0000FFFF020000008941FFFF96D1FFFF0000FFFF020000008BCAFFFF8BE0FFFF0000FFFF0400000082A9A80182D5050282B19701826D87020000FFFF0400000082A9A80182D5050282B1970182BB83010000FFFF0400000082A9A80182D50502826D870282B197010000FFFF060000004300D8024100EF02826F70024300D802826E7902826C91020000FFFF060000004300D8024100EF02826F70024300D802826E7902826D87020000FFFF060000004300D8024100EF02826F7002826AA002826E7902826C91020000FFFF060000004300D8024100EF02826F7002826AA002826E7902826D87020000FFFF06000000826AA0024100EF02826F70024300D802826E7902826C91020000FFFF06000000826AA0024100EF02826F70024300D802826E7902826D87020000FFFF06000000826AA0024100EF02826F7002826AA002826E7902826C91020000FFFF06000000826AA0024100EF02826F7002826AA002826E7902826D87020000FFFF090000008266C2024100EF0282DF2B01826C91024100EF0224005C02827353028264C702827164020000FFFF0A0000008266C2024100EF0282DF2B0124005C0282744C02826F7002826F7002826E790282716402827353020000FFFF0600000024005C028278280224005C02827353028264C702826C91020000FFFF0400000082B58F0182B78B0182C46C0182DE30010000FFFF050000008264C702827544028264C702826D8702827353020000FFFF0400000082A2C40182D72100826D870282C666010000FFFF0500000024005C02827353024100EF028265BD028265BD020000FFFF0400000082B78B0182BD7E0182C2720182D344010000FFFF08000000826E79028265BD028265BD028268AE024300D8028268AE024100EF02826B99020000FFFF0600000082A8AE0182D3440182A2C40182B58F0182E2200182E906010000FFFF0700000024005C0282744C02826F7002826F7002826E790282716402827353020000FFFF0400000082B3930182DBF301815BD20182C666010000FFFF050000004100EF028263D302826C91028268AE02826D87020000FFFF04000000A700CB0182C7400082DD3401826D87020000FFFF04000000A700CB0182C7400082DD340182BB83010000FFFF04000000A700CB0182C7400082DD340182C95D010000FFFF04000000A700CB0182C7400082DD340182C95D010000FFFF02000000895EFFFF8963FFFF0000FFFF0400000082A4BD01826D870282A6B50182A2C4010000FFFF020000008AC7FFFF979DFFFF0000FFFF0300000082A9A801826D870282E80A010000FFFF030000008AD6FFFF8C57FFFF8ED2FFFF0000FFFF030000008A4AFFFF94ADFFFF8ED2FFFF0000FFFF0400000082A8AE01926DFFFF82E70E0182B987010000FFFF0400000082A8AE0182B58F0182E70E0182B987010000FFFF020000008D90FFFF926DFFFF0000FFFF0300000088D4FFFF88C0FFFF9577FFFF0000FFFF0400000082E2200182E80A0182BF7901826D87020000FFFF0300000082E2200182ADA00182B4B7000000FFFF0400000092D2FFFF967BFFFF8F74FFFF8D4FFFFF0000FFFF0300000096D8FFFF967BFFFF8263D3020000FFFF0400000096D8FFFF967BFFFF97B4FFFF8CC8FFFF0000FFFF040000008B7BFFFF89BAFFFF4100EF02826F70020000FFFF040000008B7BFFFF89BAFFFF8B50FFFF8EF7FFFF0000FFFF08000000826C91028267B5028265BD02815BD2018266C20282BF7901815BD20182DE30010000FFFF06000000826C91028267B5028265BD0282BF7901815BD20182DE30010000FFFF040000008B43FFFF82AED40082E9060182A2C4010000FFFF0400000082ABA40182AED40082E9060182A2C4010000FFFF0200000090B8FFFF9496FFFF0000FFFF0200000088FAFFFF9790FFFF0000FFFF020000009641FFFF9550FFFF0000FFFF03000000945DFFFF968CFFFF898AFFFF0000FFFF020000008B54FFFF93AAFFFF0000FFFF0300000082DF2B0182ADA00182E70E010000FFFF0200000093F7FFFF965FFFFF0000FFFF02000000956EFFFF93FBFFFF0000FFFF0600000082BE800082C2720182BF790182EDF70082A2C40182D344010000FFFF010000009857FFFF0000FFFF0300000082C27201826D870282DA18000000FFFF020000008B68FFFF82E80A010000FFFF020000009181FFFF9852FFFF0000FFFF0500000082A2C40182DC380182E70E0182BF790182A8AE010000FFFF0500000082A8AE0182C8620182C95D0182B78B0182C666010000FFFF0200000088A4FFFF8974FFFF0000FFFF020000008AE7FFFF8ECBFFFF0000FFFF0200000095D0FFFF925BFFFF0000FFFF020000009790FFFF8CF0FFFF0000FFFF0300000082B9870182DE300182B58F010000FFFF030000008E4FFFFF82C2720182EBFC000000FFFF02000000E3E8FFFF96E5FFFF0000FFFF0200000093F7FFFF92D9FFFF0000FFFF0300000090ABFFFF8AB4FFFF91D1FFFF0000FFFF0500000082A8AE01815BD20182AAE60082B8A50082DE30010000FFFF020000008EA9FFFF88D4FFFF0000FFFF0200000093FBFFFF97D6FFFF0000FFFF020000009278FFFF9852FFFF0000FFFF020000008DD7FFFF96AFFFFF0000FFFF020000008B87FFFF96AFFFFF0000FFFF0200000094D8FFFF91B0FFFF0000FFFF02000000966BFFFF914EFFFF0000FFFF0200000093ECFFFF914EFFFF0000FFFF030000008E4FFFFF8D91FFFF906CFFFF0000FFFF0200000097BDFFFF904AFFFF0000FFFF0200000096B2FFFF90B8FFFF0000FFFF010000008FA8FFFF0000FFFF020000008FA9FFFF9577FFFF0000FFFF020000008F97FFFF8941FFFF0000FFFF020000008EE8FFFF88FAFFFF0000FFFF030000008D67FFFF96D1FFFF906CFFFF0000FFFF020000009849FFFF8F95FFFF0000FFFF020000008987FFFF8CF0FFFF0000FFFF0400000082ADA00182EBFC00826D870282DA18000000FFFF03000000A700CB0182DF2B018CF6FFFF0000FFFF0300000082A2C40182BD7E018CF6FFFF0000FFFF040000009553FFFF8AD1FFFF82C52A0082D467000000FFFF0400000082E90601826D870282D8FC01826D87020000FFFF020000008941FFFF9594FFFF0000FFFF020000008941FFFF904FFFFF0000FFFF0200000088FAFFFF8D73FFFF0000FFFF0400000082BF7901826D870282BF7901826D87020000FFFF02000000926AFFFF8DAAFFFF0000FFFF0500000082A8AE018F95FFFF82AF9B018EEBFFFF906CFFFF0000FFFF0300000082BF7901826D870296D1FFFF0000FFFF02000000914EFFFF906CFFFF0000FFFF0300000082ADA001826D870282C95D010000FFFF030000009099FFFF8959FFFF826F70020000FFFF0200000093B6FFFF92E5FFFF0000FFFF0200000095EFFFFF8C73FFFF0000FFFF020000008BE0FFFF8BCAFFFF0000FFFF0400000082A8AE0182C2720182CF170282A2C4010000FFFF0300000082BF790182ADA00182D137000000FFFF03000000A700CB0182C8620182E906010000FFFF03000000A700CB0182CA590182B78B010000FFFF0200000093FBFFFF8EF1FFFF0000FFFF01000000E453FFFF0000FFFF0200000090C2FFFF8AADFFFF0000FFFF0200000096ECFFFF8AADFFFF0000FFFF0200000095D6FFFF94E9FFFF0000FFFF020000008FACFFFF95D6FFFF0000FFFF0200000091E5FFFF95D6FFFF0000FFFF0300000082D1370082C2720182BF79010000FFFF0300000082A4BD01826D870282BF79010000FFFF020000009470FFFF906CFFFF0000FFFF020000009273FFFF8ABFFFFF0000FFFF0400000082B9870182ADA00182CD4C0182E70E010000FFFF0400000082744C02826D8702826AA002826E79020000FFFF0400000082BF7901826D870282A9A80182B78B010000FFFF0400000082CF1702826D870282C46C0182A2C4010000FFFF0200000095D6FFFF8AEDFFFF0000FFFF0400000082ABA401826D870282BD7E0182DC38010000FFFF05000000826C91024100EF02826D8702826AA002826E79020000FFFF0500000082B78B0182C66601815BD20182A9A801815BD2010000FFFF0300000082BF7901815BD20182C666010000FFFF0400000082AAE600826D870282B6AE0082E220010000FFFF000000006D7367001C080000020000009675FFFF8B4EFFFF0000FFFF030000008B43FFFF88E1FFFF82A2C4010000FFFF040000008B43FFFF82BF790182AAE60082A2C4010000FFFF0400000082ABA40182BF790182AAE60082A2C4010000FFFF030000008B43FFFF8BB6FFFF82A2C4010000FFFF0400000090B8FFFF905FFFFF9496FFFF8EE3FFFF0000FFFF04000000926DFFFF8C62FFFF9278FFFF82EA02010000FFFF0500000082BF790182A6B50182A8AE0182ADA00182EA02010000FFFF0200000096DAFFFF88C3FFFF0000FFFF0200000096BEFFFF96D3FFFF0000FFFF05000000A700CB0182ABA40182DF2B0182ADA00182E70E010000FFFF0400000082EBFC00826D870282CF170282E80A010000FFFF02000000E19AFFFF9561FFFF0000FFFF0300000082E70E0182A2C4019561FFFF0000FFFF020000009492FFFF9273FFFF0000FFFF01000000E6EAFFFF0000FFFF0300000082BF7901826D870282CE4D000000FFFF0100000098FBFFFF0000FFFF0200000098FBFFFF9943FFFF0000FFFF02000000936AFFFF8E45FFFF0000FFFF020000008CEEFFFF9048FFFF0000FFFF02000000E6CBFFFF96AFFFFF0000FFFF02000000E271FFFF91BDFFFF0000FFFF0200000094F1FFFF906CFFFF0000FFFF030000009056FFFF95BDFFFF96AFFFFF0000FFFF0400000093C1FFFF8EEAFFFF9594FFFF978EFFFF0000FFFF0500000082CE4D0082A9A80182BF790182E61201826D87020000FFFF0400000082BF790182E61201826D87028CF6FFFF0000FFFF0500000082C6660182E9060182B197019597FFFF9843FFFF0000FFFF0400000082C6660182E9060182B197018FECFFFF0000FFFF0200000096D1FFFF9382FFFF0000FFFF030000008D95FFFF826D870282DA18000000FFFF040000009573FFFF89C2FFFF9047FFFF96AFFFFF0000FFFF020000009484FFFF8F97FFFF0000FFFF0200000088FAFFFF9484FFFF0000FFFF020000008BADFFFF8AADFFFF0000FFFF0400000082E2200182E80A0182DC3801826D87020000FFFF0300000082DC3801826D870282B197010000FFFF0300000082A8AE0182DF2B0182B197010000FFFF0300000082BF7901826D870282DBF3010000FFFF0300000082BF7901826D870282DA18000000FFFF0400000082B9870182C2720182ADA00182B78B010000FFFF0300000024005C028264C702827730020000FFFF020000009484FFFF8F74FFFF0000FFFF020000009483FFFF8F74FFFF0000FFFF0300000082EA020182A2C40182D505020000FFFF0400000082B78B0182D8FC0182E9060182DC38010000FFFF0500000082ADA00182E80A0182C6660182E80A0182B78B010000FFFF0500000082D3440182A6B50182E70E0182BF790182A8AE010000FFFF0300000082B4B700815BD20196CBFFFF0000FFFF0400000082B4B700815BD20182DF2B01826D87020000FFFF0400000082B98701826D870282B8A50082E80A010000FFFF0400000082A8AE0182C8620182C95D01815BD2010000FFFF040000008265BD0282744C024300D802826AA0020000FFFF0400000082D34401A700CB0182C2720182ADA0010000FFFF0500000082BF790182E22001826D870282B1970182EBFC000000FFFF050000004200E0028268AE02827353024300D8028267B5020000FFFF040000004300D80282744C02826D8702827353020000FFFF05000000826F70028264C702826D87028268AE0224005C020000FFFF0300000082D8FC0182C95D0182B78B010000FFFF06000000827544024100EF028266C2028268AE02826D87024100EF020000FFFF0300000082CE4D0082ACDD0082C862010000FFFF0400000082D46700A700CB0182ACDD0082C862010000FFFF05000000826F700282744C0224005C0224005C02827828020000FFFF0200000090B8FFFF8974FFFF0000FFFF0400000082BF7901826D870282DBF30182B197010000FFFF0200000088FAFFFF9790FFFF0000FFFF020000009641FFFF9550FFFF0000FFFF020000008B54FFFF93AAFFFF0000FFFF0200000093F7FFFF965FFFFF0000FFFF02000000956EFFFF93FBFFFF0000FFFF0600000082BE800082C2720182BF790182EDF70082A2C40182D344010000FFFF020000009181FFFF9852FFFF0000FFFF0500000082A2C40182DC380182E70E0182BF790182A8AE010000FFFF0500000082A8AE0182C8620182C95D0182B78B0182C666010000FFFF0200000088A4FFFF8974FFFF0000FFFF020000008AE7FFFF8ECBFFFF0000FFFF020000009790FFFF8CF0FFFF0000FFFF02000000E3E8FFFF96E5FFFF0000FFFF0200000093F7FFFF92D9FFFF0000FFFF0300000090ABFFFF8AB4FFFF91D1FFFF0000FFFF0500000082A8AE01815BD20182AAE60082B8A50082DE30010000FFFF020000008EA9FFFF88D4FFFF0000FFFF0200000093FBFFFF97D6FFFF0000FFFF020000009278FFFF9852FFFF0000FFFF0200000097BDFFFF904AFFFF0000FFFF0200000096B2FFFF90B8FFFF0000FFFF010000008FA8FFFF0000FFFF020000008FA9FFFF9577FFFF0000FFFF020000008F97FFFF8941FFFF0000FFFF020000008EE8FFFF88FAFFFF0000FFFF020000008987FFFF8CF0FFFF0000FFFF0400000082E90601826D870282D8FC01826D87020000FFFF020000008941FFFF9594FFFF0000FFFF020000008941FFFF904FFFFF0000FFFF0200000088FAFFFF8D73FFFF0000FFFF02000000926AFFFF8DAAFFFF0000FFFF0300000082BF7901826D870296D1FFFF0000FFFF0200000093B6FFFF92E5FFFF0000FFFF0200000095EFFFFF8C73FFFF0000FFFF0200000093FBFFFF8EF1FFFF0000FFFF01000000E453FFFF0000FFFF0200000090C2FFFF8AADFFFF0000FFFF0200000096ECFFFF8AADFFFF0000FFFF00000000CA104E200031736B3030246D6A352E49414C4B5F7932524C714442576C316F7A434978354D65756A3143567A72315971706A72636F772D00CA1100014E2000005DF095950000000034B468235DCB72DC5DD49010020000000A00000004080A03060904080507"; + res.WriteBytes(Util.FromHexString(d)); + return res; + } + } +} diff --git a/Mhf.Server/QueueConsumer.cs b/Mhf.Server/QueueConsumer.cs new file mode 100644 index 0000000..158a405 --- /dev/null +++ b/Mhf.Server/QueueConsumer.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using Arrowgene.Services; +using Arrowgene.Services.Logging; +using Arrowgene.Services.Networking.Tcp; +using Arrowgene.Services.Networking.Tcp.Consumer; +using Arrowgene.Services.Networking.Tcp.Consumer.BlockingQueueConsumption; +using Arrowgene.Services.Networking.Tcp.Server.AsyncEvent; +using Mhf.Server.Logging; +using Mhf.Server.Model; +using Mhf.Server.Packet; +using Mhf.Server.Setting; + +namespace Mhf.Server +{ + public class QueueConsumer : IConsumer + { + public const int NoExpectedSize = -1; + + private readonly BlockingCollection[] _queues; + private readonly Thread[] _threads; + private readonly Dictionary _clientHandlers; + private readonly Dictionary _connectionHandlers; + private readonly Dictionary _connections; + private readonly MhfLogger _logger; + private readonly object _lock; + private readonly int _maxUnitOfOrder; + private MhfSetting _setting; + private volatile bool _isRunning; + + private CancellationTokenSource _cancellationTokenSource; + + public int HandlersCount => _clientHandlers.Count; + + public Action ClientDisconnected; + public Action ClientConnected; + public Action Started; + public Action Stopped; + + public QueueConsumer(MhfSetting setting, AsyncEventSettings socketSetting) + { + _setting = setting; + _logger = LogProvider.Logger(this); + _maxUnitOfOrder = socketSetting.MaxUnitOfOrder; + _queues = new BlockingCollection[_maxUnitOfOrder]; + _threads = new Thread[_maxUnitOfOrder]; + _lock = new object(); + _clientHandlers = new Dictionary(); + _connectionHandlers = new Dictionary(); + _connections = new Dictionary(); + } + + public void Clear() + { + _clientHandlers.Clear(); + _connectionHandlers.Clear(); + } + + public void AddHandler(IClientHandler clientHandler, bool overwrite = false) + { + if (overwrite) + { + if (_clientHandlers.ContainsKey(clientHandler.Id)) + { + _clientHandlers[clientHandler.Id] = clientHandler; + } + else + { + _clientHandlers.Add(clientHandler.Id, clientHandler); + } + + return; + } + + if (_clientHandlers.ContainsKey(clientHandler.Id)) + { + _logger.Error($"ClientHandlerId: {clientHandler.Id} already exists"); + } + else + { + _clientHandlers.Add(clientHandler.Id, clientHandler); + } + } + + public void AddHandler(IConnectionHandler connectionHandler, bool overwrite = false) + { + if (overwrite) + { + if (_connectionHandlers.ContainsKey(connectionHandler.Id)) + { + _connectionHandlers[connectionHandler.Id] = connectionHandler; + } + else + { + _connectionHandlers.Add(connectionHandler.Id, connectionHandler); + } + + return; + } + + if (_connectionHandlers.ContainsKey(connectionHandler.Id)) + { + _logger.Error($"ConnectionHandlerId: {connectionHandler.Id} already exists"); + } + else + { + _connectionHandlers.Add(connectionHandler.Id, connectionHandler); + } + } + + private void HandleReceived(ITcpSocket socket, byte[] data) + { + if (!socket.IsAlive) + { + return; + } + + MhfConnection connection; + lock (_lock) + { + if (!_connections.ContainsKey(socket)) + { + _logger.Error(socket, $"Client does not exist in lookup"); + return; + } + + connection = _connections[socket]; + } + + List packets = connection.Receive(data); + foreach (MhfPacket packet in packets) + { + MhfClient client = connection.Client; + if (client != null) + { + HandleReceived_Client(client, packet); + } + else + { + HandleReceived_Connection(connection, packet); + } + } + } + + private void HandleReceived_Connection(MhfConnection connection, MhfPacket packet) + { + if (!_connectionHandlers.ContainsKey(packet.Id)) + { + // _logger.LogUnknownIncomingPacket(connection, packet); + return; + } + + IConnectionHandler connectionHandler = _connectionHandlers[packet.Id]; + if (connectionHandler.ExpectedSize != NoExpectedSize && packet.Data.Size < connectionHandler.ExpectedSize) + { + _logger.Error(connection, + $"Ignoring Packed (Id:{packet.Id}) is smaller ({packet.Data.Size}) than expected ({connectionHandler.ExpectedSize})"); + return; + } + + // _logger.LogIncomingPacket(connection, packet); + packet.Data.SetPositionStart(); + try + { + connectionHandler.Handle(connection, packet); + } + catch (Exception ex) + { + _logger.Exception(connection, ex); + } + } + + private void HandleReceived_Client(MhfClient client, MhfPacket packet) + { + if (!_clientHandlers.ContainsKey(packet.Id)) + { + //_logger.LogUnknownIncomingPacket(client, packet); + return; + } + + IClientHandler clientHandler = _clientHandlers[packet.Id]; + if (clientHandler.ExpectedSize != NoExpectedSize && packet.Data.Size < clientHandler.ExpectedSize) + { + _logger.Error(client, + $"Ignoring Packed (Id:{packet.Id}) is smaller ({packet.Data.Size}) than expected ({clientHandler.ExpectedSize})"); + return; + } + + // _logger.LogIncomingPacket(client, packet); + packet.Data.SetPositionStart(); + try + { + clientHandler.Handle(client, packet); + } + catch (Exception ex) + { + _logger.Exception(client, ex); + } + } + + private void HandleDisconnected(ITcpSocket socket) + { + MhfConnection connection; + lock (_lock) + { + if (!_connections.ContainsKey(socket)) + { + _logger.Error(socket, $"Disconnected client does not exist in lookup"); + return; + } + + connection = _connections[socket]; + _connections.Remove(socket); + _logger.Debug($"Clients Count: {_connections.Count}"); + } + + Action onClientDisconnected = ClientDisconnected; + if (onClientDisconnected != null) + { + try + { + onClientDisconnected.Invoke(connection); + } + catch (Exception ex) + { + _logger.Exception(connection, ex); + } + } + + _logger.Info(connection, $"Client disconnected"); + } + + private void HandleConnected(ITcpSocket socket) + { + MhfConnection connection = new MhfConnection(socket, new PacketFactory(_setting)); + lock (_lock) + { + _connections.Add(socket, connection); + _logger.Debug($"Clients Count: {_connections.Count}"); + } + + Action onClientConnected = ClientConnected; + if (onClientConnected != null) + { + try + { + onClientConnected.Invoke(connection); + } + catch (Exception ex) + { + _logger.Exception(connection, ex); + } + } + + _logger.Info(connection, $"Client connected"); + } + + private void Consume(int unitOfOrder) + { + while (_isRunning) + { + ClientEvent clientEvent; + try + { + clientEvent = _queues[unitOfOrder].Take(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + return; + } + + switch (clientEvent.ClientEventType) + { + case ClientEventType.ReceivedData: + HandleReceived(clientEvent.Socket, clientEvent.Data); + break; + case ClientEventType.Connected: + HandleConnected(clientEvent.Socket); + break; + case ClientEventType.Disconnected: + HandleDisconnected(clientEvent.Socket); + break; + } + } + } + + void IConsumer.OnStart() + { + if (_isRunning) + { + _logger.Error($"Consumer already running."); + return; + } + + _cancellationTokenSource = new CancellationTokenSource(); + _isRunning = true; + for (int i = 0; i < _maxUnitOfOrder; i++) + { + int uuo = i; + _queues[i] = new BlockingCollection(); + _threads[i] = new Thread(() => Consume(uuo)); + _threads[i].Name = $"Consumer: {i}"; + _logger.Info($"Starting Consumer: {i}"); + _threads[i].Start(); + } + } + + public void OnStarted() + { + Action started = Started; + if (started != null) + { + started.Invoke(); + } + } + + void IConsumer.OnReceivedData(ITcpSocket socket, byte[] data) + { + _queues[socket.UnitOfOrder].Add(new ClientEvent(socket, ClientEventType.ReceivedData, data)); + } + + void IConsumer.OnClientDisconnected(ITcpSocket socket) + { + _queues[socket.UnitOfOrder].Add(new ClientEvent(socket, ClientEventType.Disconnected)); + } + + void IConsumer.OnClientConnected(ITcpSocket socket) + { + _queues[socket.UnitOfOrder].Add(new ClientEvent(socket, ClientEventType.Connected)); + } + + void IConsumer.OnStop() + { + if (!_isRunning) + { + _logger.Error($"Consumer already stopped."); + return; + } + + _isRunning = false; + _cancellationTokenSource.Cancel(); + for (int i = 0; i < _maxUnitOfOrder; i++) + { + Thread consumerThread = _threads[i]; + _logger.Info($"Shutting Consumer: {i} down..."); + Service.JoinThread(consumerThread, 10000, _logger); + _logger.Info($"Consumer: {i} ended."); + _threads[i] = null; + } + } + + public void OnStopped() + { + Action stopped = Stopped; + if (stopped != null) + { + stopped.Invoke(); + } + } + } +} diff --git a/Mhf.Server/Setting/DatabaseSetting.cs b/Mhf.Server/Setting/DatabaseSetting.cs new file mode 100644 index 0000000..3eb6304 --- /dev/null +++ b/Mhf.Server/Setting/DatabaseSetting.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Runtime.Serialization; +using Mhf.Server.Common; +using Mhf.Server.Model; + +namespace Mhf.Server.Setting +{ + [DataContract] + public class DatabaseSetting + { + public DatabaseSetting() + { + Type = DatabaseType.SQLite; + SqLiteFolder = Path.Combine(Util.ExecutingDirectory(), "Files/Database"); + Host = "localhost"; + Port = 3306; + Database = "Mhf"; + User = string.Empty; + Password = string.Empty; + + string envDbType = Environment.GetEnvironmentVariable("DB_TYPE"); + switch (envDbType) + { + case "sqlite": + Type = DatabaseType.SQLite; + break; + } + + string envDbUser = Environment.GetEnvironmentVariable("DB_USER"); + if (!string.IsNullOrEmpty(envDbUser)) + { + User = envDbUser; + } + + string envDbPass = Environment.GetEnvironmentVariable("DB_PASS"); + if (!string.IsNullOrEmpty(envDbPass)) + { + Password = envDbPass; + } + } + + public DatabaseSetting(DatabaseSetting databaseSettings) + { + Type = databaseSettings.Type; + SqLiteFolder = databaseSettings.SqLiteFolder; + Host = databaseSettings.Host; + Port = databaseSettings.Port; + User = databaseSettings.User; + Password = databaseSettings.Password; + Database = databaseSettings.Database; + } + + [DataMember(Order = 0)] public DatabaseType Type { get; set; } + + [DataMember(Order = 1)] public string SqLiteFolder { get; set; } + + [DataMember(Order = 2)] public string Host { get; set; } + + [DataMember(Order = 3)] public short Port { get; set; } + + [DataMember(Order = 4)] public string User { get; set; } + + [DataMember(Order = 5)] public string Password { get; set; } + + [DataMember(Order = 6)] public string Database { get; set; } + } +} diff --git a/Mhf.Server/Setting/MhfSetting.cs b/Mhf.Server/Setting/MhfSetting.cs new file mode 100644 index 0000000..dde4da5 --- /dev/null +++ b/Mhf.Server/Setting/MhfSetting.cs @@ -0,0 +1,105 @@ +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using Arrowgene.Services.Networking.Tcp.Server.AsyncEvent; +using Mhf.Server.Common; + +namespace Mhf.Server.Setting +{ + [DataContract] + public class MhfSetting + { + /// + /// Warning: + /// Changing while having existing accounts requires to rehash all passwords. + /// The number is log2, so adding +1 doubles the time it takes. + /// https://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ + /// + public const int BCryptWorkFactor = 10; + + [IgnoreDataMember] public IPAddress ListenIpAddress { get; set; } + + [DataMember(Name = "ListenIpAddress", Order = 0)] + public string DataListenIpAddress + { + get => ListenIpAddress.ToString(); + set => ListenIpAddress = string.IsNullOrEmpty(value) ? null : IPAddress.Parse(value); + } + + [IgnoreDataMember] public IPAddress AuthServerIpAddress { get; set; } + + [DataMember(Name = "AuthIpAddress", Order = 1)] + public string DataAuthIpAddress + { + get => AuthServerIpAddress.ToString(); + set => AuthServerIpAddress = string.IsNullOrEmpty(value) ? null : IPAddress.Parse(value); + } + + [DataMember(Order = 2)] public ushort AuthServerPort { get; set; } + + [IgnoreDataMember] public IPAddress LobbyServerIpAddress { get; set; } + + [DataMember(Name = "LobbyIpAddress", Order = 3)] + public string DataLobbyIpAddress + { + get => LobbyServerIpAddress.ToString(); + set => LobbyServerIpAddress = string.IsNullOrEmpty(value) ? null : IPAddress.Parse(value); + } + + [DataMember(Order = 4)] public ushort LobbyServerPort { get; set; } + + [DataMember(Order = 10)] public bool NeedRegistration { get; set; } + + [DataMember(Order = 20)] public int LogLevel { get; set; } + + [DataMember(Order = 21)] public bool LogUnknownIncomingPackets { get; set; } + + [DataMember(Order = 22)] public bool LogOutgoingPackets { get; set; } + + [DataMember(Order = 23)] public bool LogIncomingPackets { get; set; } + + [DataMember(Order = 40)] public string FilesFolder { get; set; } + + [DataMember(Order = 60)] public WebSetting WebSetting { get; set; } + [DataMember(Order = 70)] public DatabaseSetting DatabaseSetting { get; set; } + + [DataMember(Order = 100)] public AsyncEventSettings ServerSocketSettings { get; set; } + + public MhfSetting() + { + ListenIpAddress = IPAddress.Any; + AuthServerIpAddress = IPAddress.Loopback; + AuthServerPort = 53312; + LobbyServerIpAddress = IPAddress.Loopback; + LobbyServerPort = 53310; + NeedRegistration = false; + LogLevel = 0; + LogUnknownIncomingPackets = true; + LogOutgoingPackets = true; + LogIncomingPackets = true; + WebSetting = new WebSetting(); + DatabaseSetting = new DatabaseSetting(); + ServerSocketSettings = new AsyncEventSettings(); + ServerSocketSettings.MaxUnitOfOrder = 2; + FilesFolder = Path.Combine(Util.ExecutingDirectory(), "Files"); + } + + public MhfSetting(MhfSetting setting) + { + ListenIpAddress = setting.ListenIpAddress; + AuthServerIpAddress = setting.AuthServerIpAddress; + AuthServerPort = setting.AuthServerPort; + LobbyServerIpAddress = setting.LobbyServerIpAddress; + LobbyServerPort = setting.LobbyServerPort; + NeedRegistration = setting.NeedRegistration; + LogLevel = setting.LogLevel; + LogUnknownIncomingPackets = setting.LogUnknownIncomingPackets; + LogOutgoingPackets = setting.LogOutgoingPackets; + LogIncomingPackets = setting.LogIncomingPackets; + WebSetting = setting.WebSetting; + FilesFolder = setting.FilesFolder; + DatabaseSetting = new DatabaseSetting(setting.DatabaseSetting); + ServerSocketSettings = new AsyncEventSettings(setting.ServerSocketSettings); + } + } +} diff --git a/Mhf.Server/Setting/SettingProvider.cs b/Mhf.Server/Setting/SettingProvider.cs new file mode 100644 index 0000000..2f10f02 --- /dev/null +++ b/Mhf.Server/Setting/SettingProvider.cs @@ -0,0 +1,53 @@ +using System.IO; +using Arrowgene.Services.Serialization; +using Mhf.Server.Common; + +namespace Mhf.Server.Setting +{ + public class SettingProvider + { + private readonly string _directory; + + + public SettingProvider(string directory = null) + { + if (Directory.Exists(directory)) + { + _directory = directory; + } + else + { + _directory = Util.ExecutingDirectory(); + } + } + + public string GetSettingsPath(string file) + { + return Path.Combine(_directory, file); + } + + public void Save(T settings, string file) + { + string path = GetSettingsPath(file); + string json = JsonSerializer.Serialize(settings); + File.WriteAllText(path, json); + } + + public T Load(string file) + { + T settings; + string path = GetSettingsPath(file); + if (File.Exists(path)) + { + string json = File.ReadAllText(path); + settings = JsonSerializer.Deserialize(json); + } + else + { + settings = default(T); + } + + return settings; + } + } +} diff --git a/Mhf.Server/Setting/WebSetting.cs b/Mhf.Server/Setting/WebSetting.cs new file mode 100644 index 0000000..26c75ef --- /dev/null +++ b/Mhf.Server/Setting/WebSetting.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Runtime.Serialization; +using Mhf.Server.Common; + +namespace Mhf.Server.Setting +{ + [DataContract] + public class WebSetting + { + public WebSetting() + { + ServerHeader = null; + WebFolder = Path.Combine(Util.ExecutingDirectory(), "Files/www"); + HttpPort = 80; + HttpsEnabled = true; + HttpsPort = 443; + HttpsCertPath = Path.Combine(Util.ExecutingDirectory(), "Files/mhf.pfx"); + } + + public WebSetting(WebSetting webSetting) + { + ServerHeader = webSetting.ServerHeader; + WebFolder = webSetting.WebFolder; + HttpPort = webSetting.HttpPort; + HttpsEnabled = webSetting.HttpsEnabled; + HttpsPort = webSetting.HttpsPort; + HttpsCertPath = webSetting.HttpsCertPath; + } + + [DataMember(Order = 1)] public string ServerHeader { get; set; } + [DataMember(Order = 2)] public short HttpPort { get; set; } + [DataMember(Order = 3)] public bool HttpsEnabled { get; set; } + [DataMember(Order = 4)] public short HttpsPort { get; set; } + [DataMember(Order = 5)] public string HttpsCertPath { get; set; } + [DataMember(Order = 6)] public string WebFolder { get; set; } + } +} diff --git a/Mhf.Server/Web/Middleware/IWebMiddleware.cs b/Mhf.Server/Web/Middleware/IWebMiddleware.cs new file mode 100644 index 0000000..82a7f9d --- /dev/null +++ b/Mhf.Server/Web/Middleware/IWebMiddleware.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Middleware +{ + /// + /// Defines a middleware + /// + public interface IWebMiddleware + { + Task Handle(WebRequest request, WebMiddlewareDelegate next); + } +} diff --git a/Mhf.Server/Web/Middleware/WebMiddleware.cs b/Mhf.Server/Web/Middleware/WebMiddleware.cs new file mode 100644 index 0000000..2ba72a4 --- /dev/null +++ b/Mhf.Server/Web/Middleware/WebMiddleware.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Arrowgene.Services.Logging; + +namespace Mhf.Server.Web.Middleware +{ + public abstract class WebMiddleware : IWebMiddleware + { + protected ILogger Logger => LogProvider.Instance.GetLogger(this); + public abstract Task Handle(WebRequest request, WebMiddlewareDelegate next); + } +} diff --git a/Mhf.Server/Web/Middleware/WebMiddlewareDelegate.cs b/Mhf.Server/Web/Middleware/WebMiddlewareDelegate.cs new file mode 100644 index 0000000..5de718a --- /dev/null +++ b/Mhf.Server/Web/Middleware/WebMiddlewareDelegate.cs @@ -0,0 +1,6 @@ +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Middleware +{ + public delegate Task WebMiddlewareDelegate(WebRequest request); +} diff --git a/Mhf.Server/Web/Middleware/WebMiddlewareStack.cs b/Mhf.Server/Web/Middleware/WebMiddlewareStack.cs new file mode 100644 index 0000000..97d1426 --- /dev/null +++ b/Mhf.Server/Web/Middleware/WebMiddlewareStack.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Middleware +{ + /// + /// Implementation of a middleware + /// + public class WebMiddlewareStack + { + private WebMiddlewareDelegate _webMiddlewareDelegate; + + public WebMiddlewareStack(WebMiddlewareDelegate kernel) + { + _webMiddlewareDelegate = kernel; + } + + public Task Start(WebRequest request) + { + return _webMiddlewareDelegate(request); + } + + public void Use(Func middleware) + { + _webMiddlewareDelegate = middleware(_webMiddlewareDelegate); + } + } +} diff --git a/Mhf.Server/Web/MimeTypeMap.cs b/Mhf.Server/Web/MimeTypeMap.cs new file mode 100644 index 0000000..5661706 --- /dev/null +++ b/Mhf.Server/Web/MimeTypeMap.cs @@ -0,0 +1,655 @@ +namespace Mhf.Server.Web +{ + using System; + using System.Collections.Generic; + + public static class MimeTypeMap + { + private static readonly IDictionary _mappings = + new Dictionary(BuildMappings()); + + private static IDictionary BuildMappings() + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Monster Hunter Specific + {".xml", "application/xml"}, + {".exe", "application/x-msdownload"}, + {".dat", "application/octet-stream"}, + // General + {".323", "text/h323"}, + {".3g2", "video/3gpp2"}, + {".3gp", "video/3gpp"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".7z", "application/x-7z-compressed"}, + {".aa", "audio/audible"}, + {".AAC", "audio/aac"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".adp", "application/msaccess"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".afm", "application/octet-stream"}, + {".ai", "application/postscript"}, + {".aif", "audio/aiff"}, + {".aifc", "audio/aiff"}, + {".aiff", "audio/aiff"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".amc", "application/mpeg"}, + {".anx", "application/annodex"}, + {".apk", "application/vnd.android.package-archive"}, + {".application", "application/x-ms-application"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".asf", "video/x-ms-asf"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asm", "text/plain"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".atom", "application/atom+xml"}, + {".au", "audio/basic"}, + {".avi", "video/x-msvideo"}, + {".axa", "audio/annodex"}, + {".axs", "application/olescript"}, + {".axv", "video/annodex"}, + {".bas", "text/plain"}, + {".bcpio", "application/x-bcpio"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".cab", "application/octet-stream"}, + {".caf", "audio/x-caf"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".cc", "text/plain"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cdf", "application/x-cdf"}, + {".cer", "application/x-x509-ca-cert"}, + {".cfg", "text/plain"}, + {".chm", "application/octet-stream"}, + {".class", "application/x-java-applet"}, + {".clp", "application/x-msclip"}, + {".cmd", "text/plain"}, + {".cmx", "image/x-cmx"}, + {".cnf", "text/plain"}, + {".cod", "image/cis-cod"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cpio", "application/x-cpio"}, + {".cpp", "text/plain"}, + {".crd", "application/x-mscardfile"}, + {".crl", "application/pkix-crl"}, + {".crt", "application/x-x509-ca-cert"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csh", "application/x-csh"}, + {".csproj", "text/plain"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".cur", "application/octet-stream"}, + {".cxx", "text/plain"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dcr", "application/x-director"}, + {".def", "text/plain"}, + {".deploy", "application/octet-stream"}, + {".der", "application/x-x509-ca-cert"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".dir", "application/x-director"}, + {".disco", "text/xml"}, + {".divx", "video/divx"}, + {".dll", "application/x-msdownload"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".doc", "application/msword"}, + {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dot", "application/msword"}, + {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtd", "text/xml"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dvi", "application/x-dvi"}, + {".dwf", "drawing/x-dwf"}, + {".dwg", "application/acad"}, + {".dwp", "application/octet-stream"}, + {".dxf", "application/x-dxf"}, + {".dxr", "application/x-director"}, + {".eml", "message/rfc822"}, + {".emz", "application/octet-stream"}, + {".eot", "application/vnd.ms-fontobject"}, + {".eps", "application/postscript"}, + {".es", "application/ecmascript"}, + {".etl", "application/etl"}, + {".etx", "text/x-setext"}, + {".evy", "application/envoy"}, + + {".exe.config", "text/xml"}, + {".f4v", "video/mp4"}, + {".fdf", "application/vnd.fdf"}, + {".fif", "application/fractals"}, + {".filters", "application/xml"}, + {".fla", "application/octet-stream"}, + {".flac", "audio/flac"}, + {".flr", "x-world/x-vrml"}, + {".flv", "video/x-flv"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".gif", "image/gif"}, + {".gpx", "application/gpx+xml"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".hdf", "application/x-hdf"}, + {".hdml", "text/x-hdml"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hlp", "application/winhlp"}, + {".hpp", "text/plain"}, + {".hqx", "application/mac-binhex40"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".ico", "image/x-icon"}, + {".ics", "application/octet-stream"}, + {".idl", "text/plain"}, + {".ief", "image/ief"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".ini", "text/plain"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".isma", "application/octet-stream"}, + {".ismv", "application/octet-stream"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jar", "application/java-archive"}, + {".java", "application/octet-stream"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".jpb", "application/octet-stream"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".latex", "application/x-latex"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".lzh", "application/octet-stream"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".m1v", "video/mpeg"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m2v", "video/mpeg"}, + {".m3u", "audio/x-mpegurl"}, + {".m3u8", "audio/x-mpegurl"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".m4v", "video/x-m4v"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".man", "application/x-troff-man"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mbox", "application/mbox"}, + {".mda", "application/msaccess"}, + {".mdb", "application/x-msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".me", "application/x-troff-me"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mid", "audio/mid"}, + {".midi", "audio/mid"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mk3d", "video/x-matroska-3d"}, + {".mka", "audio/x-matroska"}, + {".mkv", "video/x-matroska"}, + {".mmf", "application/x-smaf"}, + {".mno", "text/xml"}, + {".mny", "application/x-msmoney"}, + {".mod", "video/mpeg"}, + {".mov", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".mp2", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpa", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpg", "video/mpeg"}, + {".mpp", "application/vnd.ms-project"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".ms", "application/x-troff-ms"}, + {".msg", "application/vnd.ms-outlook"}, + {".msi", "application/octet-stream"}, + {".mso", "application/octet-stream"}, + {".mts", "video/vnd.dlna.mpeg-tts"}, + {".mtx", "application/xml"}, + {".mvb", "application/x-msmediaview"}, + {".mvc", "application/x-miva-compiled"}, + {".mxf", "application/mxf"}, + {".mxp", "application/x-mmxp"}, + {".nc", "application/x-netcdf"}, + {".nsc", "video/x-ms-asf"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".oda", "application/oda"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".odh", "text/plain"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".odl", "text/plain"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".opus", "audio/ogg"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".otf", "application/font-sfnt"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".p10", "application/pkcs10"}, + {".p12", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".p7c", "application/pkcs7-mime"}, + {".p7m", "application/pkcs7-mime"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".p7s", "application/pkcs7-signature"}, + {".pbm", "image/x-portable-bitmap"}, + {".pcast", "application/x-podcast"}, + {".pct", "image/pict"}, + {".pcx", "application/octet-stream"}, + {".pcz", "application/octet-stream"}, + {".pdf", "application/pdf"}, + {".pfb", "application/octet-stream"}, + {".pfm", "application/octet-stream"}, + {".pfx", "application/x-pkcs12"}, + {".pgm", "image/x-portable-graymap"}, + {".pic", "image/pict"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pls", "audio/scpls"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pml", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".png", "image/png"}, + {".pnm", "image/x-portable-anymap"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {".ppm", "image/x-portable-pixmap"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prf", "application/pics-rules"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".ps", "application/postscript"}, + {".psc1", "application/PowerShell"}, + {".psd", "application/octet-stream"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pst", "application/vnd.ms-outlook"}, + {".pub", "application/x-mspublisher"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qt", "video/quicktime"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".qxd", "application/octet-stream"}, + {".ra", "audio/x-pn-realaudio"}, + {".ram", "audio/x-pn-realaudio"}, + {".rar", "application/x-rar-compressed"}, + {".ras", "image/x-cmu-raster"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".reg", "text/plain"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgb", "image/x-rgb"}, + {".rgs", "text/plain"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmi", "audio/mid"}, + {".rmp", "application/vnd.rn-rn_music_package"}, + {".roff", "application/x-troff"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".rtf", "application/rtf"}, + {".rtx", "text/richtext"}, + {".rvt", "application/octet-stream"}, + {".ruleset", "application/xml"}, + {".s", "text/plain"}, + {".safariextz", "application/x-safari-safariextz"}, + {".scd", "application/x-msschedule"}, + {".scr", "text/plain"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sdp", "application/sdp"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".sgml", "text/sgml"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".shtml", "text/html"}, + {".sit", "application/x-stuffit"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".skp", "application/x-koan"}, + {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smi", "application/octet-stream"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snd", "audio/basic"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".spc", "application/x-pkcs7-certificates"}, + {".spl", "application/futuresplash"}, + {".spx", "audio/ogg"}, + {".src", "application/x-wais-source"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".svc", "application/xml"}, + {".svg", "image/svg+xml"}, + {".swf", "application/x-shockwave-flash"}, + {".step", "application/step"}, + {".stp", "application/step"}, + {".t", "application/x-troff"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tex", "application/x-tex"}, + {".texi", "application/x-texinfo"}, + {".texinfo", "application/x-texinfo"}, + {".tgz", "application/x-compressed"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".thn", "application/octet-stream"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".tr", "application/x-troff"}, + {".trm", "application/x-msterminal"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tsv", "text/tab-separated-values"}, + {".ttf", "application/font-sfnt"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".txt", "text/plain"}, + {".u32", "application/octet-stream"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".ustar", "application/x-ustar"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcf", "text/x-vcard"}, + {".vcproj", "application/xml"}, + {".vcs", "text/plain"}, + {".vcxproj", "application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsd", "application/vnd.visio"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vss", "application/vnd.visio"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vst", "application/vnd.visio"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsw", "application/vnd.visio"}, + {".vsx", "application/vnd.visio"}, + {".vtt", "text/vtt"}, + {".vtx", "application/vnd.visio"}, + {".wasm", "application/wasm"}, + {".wav", "audio/wav"}, + {".wave", "audio/wav"}, + {".wax", "audio/x-ms-wax"}, + {".wbk", "application/msword"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wdp", "image/vnd.ms-photo"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".wks", "application/vnd.ms-works"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wm", "video/x-ms-wm"}, + {".wma", "audio/x-ms-wma"}, + {".wmd", "application/x-ms-wmd"}, + {".wmf", "application/x-msmetafile"}, + {".wml", "text/vnd.wap.wml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wmp", "video/x-ms-wmp"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wmz", "application/x-ms-wmz"}, + {".woff", "application/font-woff"}, + {".woff2", "application/font-woff2"}, + {".wpl", "application/vnd.ms-wpl"}, + {".wps", "application/vnd.ms-works"}, + {".wri", "application/x-mswrite"}, + {".wrl", "x-world/x-vrml"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".wsdl", "text/xml"}, + {".wvx", "video/x-ms-wvx"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xaml", "application/xaml+xml"}, + {".xap", "application/x-silverlight-app"}, + {".xbap", "application/x-ms-xbap"}, + {".xbm", "image/x-xbitmap"}, + {".xdr", "text/plain"}, + {".xht", "application/xhtml+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xla", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {".xlc", "application/vnd.ms-excel"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xlt", "application/vnd.ms-excel"}, + {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".xlw", "application/vnd.ms-excel"}, + {".xmp", "application/octet-stream"}, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xpm", "image/x-xpixmap"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsl", "text/xml"}, + {".xslt", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xspf", "application/xspf+xml"}, + {".xtp", "application/octet-stream"}, + {".xwd", "image/x-xwindowdump"}, + {".z", "application/x-compress"}, + {".zip", "application/zip"} + }; + return mappings; + } + + public static string GetMimeType(string extension) + { + if (extension == null) + { + return null; + } + + if (!extension.StartsWith(".")) + { + extension = "." + extension; + } + + string mime; + + return _mappings.TryGetValue(extension, out mime) ? mime : "application/octet-stream"; + } + } +} diff --git a/Mhf.Server/Web/Route/FileWebRoute.cs b/Mhf.Server/Web/Route/FileWebRoute.cs new file mode 100644 index 0000000..7a4d008 --- /dev/null +++ b/Mhf.Server/Web/Route/FileWebRoute.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.Web.Route +{ + public abstract class FileWebRoute : WebRoute + { + protected IFileProvider FileProvider { get; } + + public FileWebRoute(IFileProvider fileProvider) + { + FileProvider = fileProvider; + } + } +} diff --git a/Mhf.Server/Web/Route/IWebRoute.cs b/Mhf.Server/Web/Route/IWebRoute.cs new file mode 100644 index 0000000..f3d0d08 --- /dev/null +++ b/Mhf.Server/Web/Route/IWebRoute.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Route +{ + /// + /// Defines a handler for a route + /// + public interface IWebRoute + { + string Route { get; } + Task Get(WebRequest request); + Task Post(WebRequest request); + Task Put(WebRequest request); + Task Delete(WebRequest request); + } +} diff --git a/Mhf.Server/Web/Route/IWebRouter.cs b/Mhf.Server/Web/Route/IWebRouter.cs new file mode 100644 index 0000000..2d4ebad --- /dev/null +++ b/Mhf.Server/Web/Route/IWebRouter.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Route +{ + public interface IWebRouter + { + void AddRoute(IWebRoute route); + Task Route(WebRequest request); + } +} diff --git a/Mhf.Server/Web/Route/ServerWebRoute.cs b/Mhf.Server/Web/Route/ServerWebRoute.cs new file mode 100644 index 0000000..bef66f8 --- /dev/null +++ b/Mhf.Server/Web/Route/ServerWebRoute.cs @@ -0,0 +1,12 @@ +namespace Mhf.Server.Web.Route +{ + public abstract class ServerWebRoute : WebRoute + { + protected MhfServer Server { get; } + + public ServerWebRoute(MhfServer server) + { + Server = server; + } + } +} diff --git a/Mhf.Server/Web/Route/WebRoute.cs b/Mhf.Server/Web/Route/WebRoute.cs new file mode 100644 index 0000000..c41880a --- /dev/null +++ b/Mhf.Server/Web/Route/WebRoute.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Arrowgene.Services.Logging; + +namespace Mhf.Server.Web.Route +{ + /// + /// Implementation of Kestrel server as backend + /// + public abstract class WebRoute : IWebRoute + { + protected ILogger Logger => LogProvider.Instance.GetLogger(this); + + public abstract string Route { get; } + + public virtual Task Get(WebRequest request) + { + return WebResponse.NotFound(); + } + + public virtual Task Post(WebRequest request) + { + return WebResponse.NotFound(); + } + + public virtual Task Put(WebRequest request) + { + return WebResponse.NotFound(); + } + + public virtual Task Delete(WebRequest request) + { + return WebResponse.NotFound(); + } + } +} diff --git a/Mhf.Server/Web/Route/WebRouter.cs b/Mhf.Server/Web/Route/WebRouter.cs new file mode 100644 index 0000000..80d7eed --- /dev/null +++ b/Mhf.Server/Web/Route/WebRouter.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Arrowgene.Services.Logging; +using Mhf.Server.Setting; + +namespace Mhf.Server.Web.Route +{ + /// + /// Parses routes and calls the route + /// + public class WebRouter : IWebRouter + { + private Dictionary _routes; + private ILogger _logger; + private MhfSetting _setting; + + public WebRouter(MhfSetting setting) + { + _setting = setting; + _logger = LogProvider.Instance.GetLogger(this); + _routes = new Dictionary(); + } + + + /// + /// Adds a handler for a specific route. + /// + public void AddRoute(IWebRoute route) + { + _routes.Add(route.Route, route); + } + + /// + /// Passes incoming requests to the correct route + /// + public async Task Route(WebRequest request) + { + _logger.Info($"Request: {request}"); + if (request.Path == null) + { + _logger.Error($"Request path not set, please check sever request mapping implementation"); + return await WebResponse.InternalServerError(); + } + + if (_routes.ContainsKey(request.Path)) + { + IWebRoute route = _routes[request.Path]; + Task responseTask = null; + switch (request.Method) + { + case WebRequestMethod.Get: + responseTask = route.Get(request); + break; + case WebRequestMethod.Post: + responseTask = route.Post(request); + break; + case WebRequestMethod.Put: + responseTask = route.Put(request); + break; + case WebRequestMethod.Delete: + responseTask = route.Delete(request); + break; + } + + if (responseTask == null) + { + _logger.Info($"Request method: {request.Method} not supported for requested path: {request.Path}"); + return await WebResponse.InternalServerError(); + } + + WebResponse response = await responseTask; + response.RouteFound = true; + if (!string.IsNullOrEmpty(_setting.WebSetting.ServerHeader)) + { + response.Header.Add("Server", _setting.WebSetting.ServerHeader); + } + + return response; + } + + return await WebResponse.NotFound(); + } + } +} diff --git a/Mhf.Server/Web/Server/IWebServer.cs b/Mhf.Server/Web/Server/IWebServer.cs new file mode 100644 index 0000000..f1d9888 --- /dev/null +++ b/Mhf.Server/Web/Server/IWebServer.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Mhf.Server.Web.Route; + +namespace Mhf.Server.Web.Server +{ + /// + /// Defines web server + /// + public interface IWebServer + { + void SetHandler(IWebServerHandler handler); + Task Start(); + Task Stop(); + } +} diff --git a/Mhf.Server/Web/Server/IWebServerHandler.cs b/Mhf.Server/Web/Server/IWebServerHandler.cs new file mode 100644 index 0000000..8c272c4 --- /dev/null +++ b/Mhf.Server/Web/Server/IWebServerHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Mhf.Server.Web.Server +{ + public interface IWebServerHandler + { + Task Handle(WebRequest request); + } +} diff --git a/Mhf.Server/Web/Server/Kestrel/KestrelLogger.cs b/Mhf.Server/Web/Server/Kestrel/KestrelLogger.cs new file mode 100644 index 0000000..18265a6 --- /dev/null +++ b/Mhf.Server/Web/Server/Kestrel/KestrelLogger.cs @@ -0,0 +1,61 @@ +using System; +using Arrowgene.Services.Logging; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Mhf.Server.Web.Server.Kestrel +{ + public class KestrelLogger : ILogger + { + private readonly string _name; + private Arrowgene.Services.Logging.ILogger _logger; + + public KestrelLogger(string name) + { + _name = name; + _logger = LogProvider.Logger(name); + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (exception != null) + { + _logger.Exception(exception); + return; + } + + string message = $"{formatter(state, exception)}"; + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + case LogLevel.None: + case LogLevel.Information: + _logger.Debug(message); + break; + case LogLevel.Warning: + _logger.Info(message); + break; + case LogLevel.Error: + case LogLevel.Critical: + _logger.Error(message); + break; + } + } + } +} diff --git a/Mhf.Server/Web/Server/Kestrel/KestrelLoggerProvider.cs b/Mhf.Server/Web/Server/Kestrel/KestrelLoggerProvider.cs new file mode 100644 index 0000000..929bc0d --- /dev/null +++ b/Mhf.Server/Web/Server/Kestrel/KestrelLoggerProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace Mhf.Server.Web.Server.Kestrel +{ + public class KestrelLoggerProvider: ILoggerProvider + { + private readonly ConcurrentDictionary _loggers = new ConcurrentDictionary(); + + public KestrelLoggerProvider() + { + + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, name => new KestrelLogger(name)); + } + + public void Dispose() + { + _loggers.Clear(); + } + } +} diff --git a/Mhf.Server/Web/Server/Kestrel/KestrelWebServer.cs b/Mhf.Server/Web/Server/Kestrel/KestrelWebServer.cs new file mode 100644 index 0000000..d87a32d --- /dev/null +++ b/Mhf.Server/Web/Server/Kestrel/KestrelWebServer.cs @@ -0,0 +1,198 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Arrowgene.Services.Logging; +using Mhf.Server.Setting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ILogger = Arrowgene.Services.Logging.ILogger; + +namespace Mhf.Server.Web.Server.Kestrel +{ + /// + /// Implementation of Kestrel server as backend + /// + public class KestrelWebServer : IWebServer + { + private CancellationTokenSource _cancellationTokenSource; + private ApplicationLifetime _applicationLifetime; + private IServer _server; + private int _shutdownTimeout = 10000; + private IWebServerHandler _handler; + private MhfSetting _setting; + private ILogger _logger; + + public KestrelWebServer(MhfSetting setting) + { + _logger = LogProvider.Logger(this); + _setting = setting; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public void SetHandler(IWebServerHandler handler) + { + _handler = handler; + } + + public async Task Start() + { + IHttpApplication app; + try + { + if (_handler == null) + { + throw new Exception("Missing Handler - Call SetHandler()"); + } + + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(new KestrelLoggerProvider()); + ServiceCollection services = new ServiceCollection(); + services.AddSingleton(loggerFactory); + services.AddLogging(); + + IServiceProvider serviceProvider = GetProviderFromFactory(services); + IOptions kestrelServerOptions = Options.Create(new KestrelServerOptions()); + kestrelServerOptions.Value.ApplicationServices = serviceProvider; + kestrelServerOptions.Value.ListenAnyIP(_setting.WebSetting.HttpPort); + if (_setting.WebSetting.HttpsEnabled) + { + kestrelServerOptions.Value.ListenAnyIP(_setting.WebSetting.HttpsPort, + listenOptions => + { + X509Certificate2 cert = new X509Certificate2(_setting.WebSetting.HttpsCertPath); + listenOptions.UseHttps(cert); + }); + } + + kestrelServerOptions.Value.AddServerHeader = false; + + IOptions socketTransportOptions = Options.Create(new SocketTransportOptions()); + _applicationLifetime = new ApplicationLifetime( + loggerFactory.CreateLogger() + ); + ITransportFactory transportFactory = new SocketTransportFactory( + socketTransportOptions, _applicationLifetime, loggerFactory + ); + + + _server = new KestrelServer(kestrelServerOptions, transportFactory, loggerFactory); + DiagnosticListener diagnosticListener = new DiagnosticListener("a"); + IOptions formOptions = Options.Create(new FormOptions()); + IHttpContextFactory httpContextFactory = new HttpContextFactory(formOptions); + app = new HostingApplication( + RequestDelegate, + loggerFactory.CreateLogger(), + diagnosticListener, + httpContextFactory + ); + } + catch (Exception ex) + { + _logger.Exception(ex); + return; + } + + Task kestrelStartup = _server.StartAsync(app, _cancellationTokenSource.Token); + await kestrelStartup; + _cancellationTokenSource.Token.Register( + state => ((IApplicationLifetime) state).StopApplication(), + _applicationLifetime + ); + TaskCompletionSource completionSource = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously + ); + _applicationLifetime.ApplicationStopping.Register( + obj => ((TaskCompletionSource) obj).TrySetResult(null), + completionSource + ); + Task kestrelCompleted = completionSource.Task; + object kestrelCompletedResult = await kestrelCompleted; + Task kestrelShutdown = _server.StopAsync(new CancellationToken()); + await kestrelShutdown; + } + + public async Task Stop() + { + CancellationToken token = new CancellationTokenSource(_shutdownTimeout).Token; + _applicationLifetime?.StopApplication(); + if (_server != null) + { + await _server.StopAsync(token).ConfigureAwait(false); + } + + _applicationLifetime?.NotifyStopped(); + HostingEventSource.Log.HostStop(); + } + + /// + /// Called whenever a web request arrives. + /// - Maps Kestrel HttpRequest/HttpResponse to WebRequest/WebResponse + /// - Calls router to handle the request + /// + private async Task RequestDelegate(HttpContext context) + { + WebRequest request = new WebRequest(); + request.Host = context.Request.Host.Host; + request.Port = context.Request.Host.Port; + request.Method = WebRequest.ParseMethod(context.Request.Method); + request.Path = context.Request.Path; + request.Scheme = context.Request.Scheme; + request.ContentType = context.Request.ContentType; + request.QueryString = context.Request.QueryString.Value; + request.ContentLength = context.Request.ContentLength; + foreach (string key in context.Request.Headers.Keys) + { + request.Header.Add(key, context.Request.Headers[key]); + } + + foreach (string key in context.Request.Query.Keys) + { + request.QueryParameter.Add(key, context.Request.Query[key]); + } + + foreach (string key in context.Request.Cookies.Keys) + { + request.Cookies.Add(key, context.Request.Cookies[key]); + } + + await context.Request.Body.CopyToAsync(request.Body); + request.Body.Position = 0; + WebResponse response = await _handler.Handle(request); + context.Response.StatusCode = response.StatusCode; + foreach (string key in response.Header.Keys) + { + context.Response.Headers.Add(key, response.Header[key]); + } + + response.Body.Position = 0; + await response.Body.CopyToAsync(context.Response.Body); + } + + private IServiceProvider GetProviderFromFactory(IServiceCollection collection) + { + ServiceProvider provider = collection.BuildServiceProvider(); + IServiceProviderFactory service = + provider.GetService>(); + if (service == null || service is DefaultServiceProviderFactory) + { + return provider; + } + + using (provider) + { + return service.CreateServiceProvider(service.CreateBuilder(collection)); + } + } + } +} diff --git a/Mhf.Server/Web/WebCollection.cs b/Mhf.Server/Web/WebCollection.cs new file mode 100644 index 0000000..53404fa --- /dev/null +++ b/Mhf.Server/Web/WebCollection.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace Mhf.Server.Web +{ + public class WebCollection + { + private Dictionary _collection; + private Func _keyTransformer; + + public WebCollection() : this(null) + { + } + + public WebCollection(Func keyTransformer) + { + _collection = new Dictionary(); + _keyTransformer = keyTransformer; + } + + public ICollection Keys => _collection.Keys; + + public void Add(TKey key, TValue value) + { + if (_keyTransformer != null) + { + key = _keyTransformer(key); + } + + _collection.Add(key, value); + } + + public TValue Get(TKey key) + { + if (_collection.TryGetValue(key, out TValue value)) + { + return value; + } + + return default(TValue); + } + + public bool ContainsKey(TKey key) + { + return _collection.ContainsKey(key); + } + + public TValue this[TKey key] + { + get => _collection[key]; + set => _collection[key] = value; + } + } +} diff --git a/Mhf.Server/Web/WebRequest.cs b/Mhf.Server/Web/WebRequest.cs new file mode 100644 index 0000000..c6e0f06 --- /dev/null +++ b/Mhf.Server/Web/WebRequest.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Mhf.Server.Common; + +namespace Mhf.Server.Web +{ + public class WebRequest + { + public static WebRequestMethod ParseMethod(string method) + { + method = method.ToLowerInvariant(); + switch (method) + { + case "get": return WebRequestMethod.Get; + case "put": return WebRequestMethod.Put; + case "post": return WebRequestMethod.Post; + case "delete": return WebRequestMethod.Delete; + } + + return WebRequestMethod.Unknown; + } + + public WebRequestMethod Method { get; set; } + public string Host { get; set; } + public int? Port { get; set; } + public string Path { get; set; } + public string Scheme { get; set; } + public string ContentType { get; set; } + public string QueryString { get; set; } + public long? ContentLength { get; set; } + [XmlIgnore] public Stream Body { get; set; } + public WebCollection Header { get; } + public WebCollection QueryParameter { get; } + public WebCollection Cookies { get; } + + public WebRequest() + { + Path = null; + Body = new MemoryStream(); + Header = new WebCollection(key => key.ToLowerInvariant()); + Cookies = new WebCollection(key => key.ToLowerInvariant()); + QueryParameter = new WebCollection(key => key.ToLowerInvariant()); + } + + /// + /// Clears the body. + /// + public void ClearBody() + { + Body.Position = 0; + Body.SetLength(0); + } + + /// + /// Writes give bytes from current position till length of bytes. + /// + public Task WriteAsync(byte[] bytes) + { + return WriteAsync(bytes, Body.Position); + } + + /// + /// Writes give bytes from given position till length of bytes. + /// + public Task WriteAsync(byte[] bytes, long position) + { + Body.Position = position; + return Body.WriteAsync(bytes, 0, bytes.Length); + } + + public Task ReadBytesAsync() + { + Body.Position = 0; + return Util.ReadAsync(Body); + } + + public Task ReadStringAsync() + { + return ReadStringAsync(Encoding.UTF8); + } + + public async Task ReadStringAsync(Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + Body.Position = 0; + byte[] body = await Util.ReadAsync(Body); + return encoding.GetString(body); + } + + public override string ToString() + { + return $"{Method} {Scheme}://{Host}:{Port?.ToString()}{Path}{QueryString}"; + } + } +} diff --git a/Mhf.Server/Web/WebRequestMethod.cs b/Mhf.Server/Web/WebRequestMethod.cs new file mode 100644 index 0000000..6c29a35 --- /dev/null +++ b/Mhf.Server/Web/WebRequestMethod.cs @@ -0,0 +1,11 @@ +namespace Mhf.Server.Web +{ + public enum WebRequestMethod + { + Unknown = -1, + Get = 0, + Post = 1, + Put = 2, + Delete = 3 + } +} diff --git a/Mhf.Server/Web/WebResponse.cs b/Mhf.Server/Web/WebResponse.cs new file mode 100644 index 0000000..a2213e4 --- /dev/null +++ b/Mhf.Server/Web/WebResponse.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.Web +{ + public class WebResponse + { + public static async Task NotFound() + { + WebResponse notFound = new WebResponse(); + notFound.StatusCode = 404; + await notFound.WriteAsync("404 - route not found"); + return notFound; + } + + public static async Task InternalServerError() + { + WebResponse internalServerError = new WebResponse(); + internalServerError.StatusCode = 500; + await internalServerError.WriteAsync("500 - an internal error occured"); + return internalServerError; + } + + public Stream Body { get; set; } + public int StatusCode { get; set; } + public bool RouteFound { get; set; } + + public WebCollection Header { get; } + + public WebResponse() + { + Body = new MemoryStream(); + Header = new WebCollection(); + RouteFound = false; + } + + public Task WriteAsync(IFileInfo fileInfo, bool contentLength = true) + { + if (contentLength) + { + Header.Add("content-length", $"{fileInfo.Length}"); + } + + return fileInfo.CreateReadStream().CopyToAsync(Body); + } + + public Task WriteAsync(string text, bool contentLength = true) + { + if (text == null) + throw new ArgumentNullException(nameof(text)); + return WriteAsync(text, Encoding.UTF8, contentLength); + } + + public Task WriteAsync(string text, Encoding encoding, bool contentLength = true) + { + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + byte[] bytes = encoding.GetBytes(text); + if (contentLength) + { + Header.Add("content-length", $"{bytes.Length}"); + } + + return Body.WriteAsync(bytes, 0, bytes.Length); + } + } +} diff --git a/Mhf.Server/Web/WebServer.cs b/Mhf.Server/Web/WebServer.cs new file mode 100644 index 0000000..f43badd --- /dev/null +++ b/Mhf.Server/Web/WebServer.cs @@ -0,0 +1,65 @@ +using System.Threading.Tasks; +using Arrowgene.Services.Logging; +using Mhf.Server.Setting; +using Mhf.Server.Web.Middleware; +using Mhf.Server.Web.Route; +using Mhf.Server.Web.Server; + +namespace Mhf.Server.Web +{ + /// + /// Manages web requests + /// + public class WebServer : IWebServerHandler + { + private IWebServer _server; + private WebRouter _router; + private WebMiddlewareStack _middlewareStack; + private ILogger _logger; + + public WebServer(MhfSetting setting, IWebServer server) + { + _logger = LogProvider.Logger(this); + _server = server; + _router = new WebRouter(setting); + _server.SetHandler(this); + _middlewareStack = new WebMiddlewareStack(_router.Route); + } + + public async Task Start() + { + await _server.Start(); + } + + public async Task Stop() + { + await _server.Stop(); + } + + public async Task Handle(WebRequest request) + { + WebResponse response = await _middlewareStack.Start(request); + if (!response.RouteFound) + { + _logger.Info($"No route or middleware registered for requested Path: {request.Path}"); + } + return response; + } + + public void AddRoute(IWebRoute route) + { + _router.AddRoute(route); + } + + public void AddMiddleware(IWebMiddleware middleware) + { + _middlewareStack.Use(next => req => middleware.Handle(req, next)); + // middleware.Use( + // next => req => + // { + // return next(req); + // } + // ); + } + } +} diff --git a/Mhf.Server/WebMiddlewares/StaticFileMiddleware.cs b/Mhf.Server/WebMiddlewares/StaticFileMiddleware.cs new file mode 100644 index 0000000..b966468 --- /dev/null +++ b/Mhf.Server/WebMiddlewares/StaticFileMiddleware.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Middleware; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.WebMiddlewares +{ + public class StaticFileMiddleware : IWebMiddleware + { + private string _root; + private IFileProvider _provider; + + public StaticFileMiddleware(string root, IFileProvider provider) + { + _root = root; + _provider = provider; + } + + public async Task Handle(WebRequest request, WebMiddlewareDelegate next) + { + WebResponse response = await next(request); + if (!response.RouteFound && !string.IsNullOrEmpty(request.Path)) + { + IFileInfo file = _provider.GetFileInfo(request.Path); + if (file.Exists) + { + response.RouteFound = true; + response = new WebResponse(); + response.StatusCode = 200; + string mimeType = MimeTypeMap.GetMimeType(Path.GetExtension(file.Name)); + response.Header.Add("content-type", mimeType); + await response.WriteAsync(file); + } + } + + return response; + } + } +} diff --git a/Mhf.Server/WebRoutes/AuthLauncherLoginRoute.cs b/Mhf.Server/WebRoutes/AuthLauncherLoginRoute.cs new file mode 100644 index 0000000..edba6ff --- /dev/null +++ b/Mhf.Server/WebRoutes/AuthLauncherLoginRoute.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Route; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.WebRoutes +{ + public class AuthLauncherLoginRoute : FileWebRoute + { + public AuthLauncherLoginRoute(IFileProvider fileProvider) : base(fileProvider) + { + } + + public override string Route => "/auth/launcher/login"; + + public override async Task Post(WebRequest request) + { + WebResponse response = new WebResponse(); + response.StatusCode = 200; + response.Header.Add("content-type", "text/html; charset=UTF-8"); + IFileInfo startHtml = FileProvider.GetFileInfo("auth/launcher/login.html"); + await response.WriteAsync(startHtml); + return response; + } + } +} diff --git a/Mhf.Server/WebRoutes/AuthLauncherStartRoute.cs b/Mhf.Server/WebRoutes/AuthLauncherStartRoute.cs new file mode 100644 index 0000000..0225c4f --- /dev/null +++ b/Mhf.Server/WebRoutes/AuthLauncherStartRoute.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Route; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.WebRoutes +{ + public class AuthLauncherStartRoute : FileWebRoute + { + public AuthLauncherStartRoute(IFileProvider fileProvider) : base(fileProvider) + { + } + + public override string Route => "/auth/launcher/start.html"; + + public override async Task Get(WebRequest request) + { + WebResponse response = new WebResponse(); + response.StatusCode = 200; + response.Header.Add("content-type", "text/html; charset=UTF-8"); + IFileInfo startHtml = FileProvider.GetFileInfo("auth/launcher/start.html"); + await response.WriteAsync(startHtml); + return response; + } + } +} diff --git a/Mhf.Server/WebRoutes/IndexRoute.cs b/Mhf.Server/WebRoutes/IndexRoute.cs new file mode 100644 index 0000000..355f54d --- /dev/null +++ b/Mhf.Server/WebRoutes/IndexRoute.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Route; + +namespace Mhf.Server.WebRoutes +{ + public class IndexRoute : WebRoute + { + public override string Route => "/"; + + public override async Task Get(WebRequest request) + { + WebResponse response = new WebResponse(); + response.StatusCode = 200; + await response.WriteAsync("Welcome - Index Page!"); + return response; + } + } +} diff --git a/Mhf.Server/WebRoutes/LauncherIndexRoute.cs b/Mhf.Server/WebRoutes/LauncherIndexRoute.cs new file mode 100644 index 0000000..f40ee5c --- /dev/null +++ b/Mhf.Server/WebRoutes/LauncherIndexRoute.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Route; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.WebRoutes +{ + public class LauncherIndexRoute : FileWebRoute + { + public LauncherIndexRoute(IFileProvider fileProvider) : base(fileProvider) + { + } + + public override string Route => "/launcher/"; + + public override async Task Get(WebRequest request) + { + WebResponse response = new WebResponse(); + response.StatusCode = 200; + response.Header.Add("content-type", "text/html; charset=UTF-8"); + IFileInfo startHtml = FileProvider.GetFileInfo("launcher/index.html"); + await response.WriteAsync(startHtml); + return response; + } + } +} diff --git a/Mhf.Server/WebRoutes/MhfFileRoute.cs b/Mhf.Server/WebRoutes/MhfFileRoute.cs new file mode 100644 index 0000000..a9ba7e6 --- /dev/null +++ b/Mhf.Server/WebRoutes/MhfFileRoute.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Mhf.Server.Web; +using Mhf.Server.Web.Route; +using Microsoft.Extensions.FileProviders; + +namespace Mhf.Server.WebRoutes +{ + public class MhfFileRoute : FileWebRoute + { + public MhfFileRoute(IFileProvider fileProvider) : base(fileProvider) + { + } + + public override string Route => "/mhf_file.php"; + + public override async Task Get(WebRequest request) + { + if (request.QueryParameter.ContainsKey("key")) + { + IFileInfo mhfFile = FileProvider.GetFileInfo("MHFUP_00.DAT"); + WebResponse response = new WebResponse(); + response.StatusCode = 200; + response.Header.Add("content-type", "application/octet-stream"); + response.Header.Add("content-disposition", "inline; filename=\"MHFUP_00.DAT\""); + response.Header.Add("connection", "close"); + await response.WriteAsync(mhfFile); + return response; + } + + if (request.QueryParameter.ContainsKey("chk")) + { + WebResponse response = new WebResponse(); + response.StatusCode = 200; + response.Header.Add("content-type", "application/octet-stream"); + response.Header.Add("connection", "close"); + await response.WriteAsync("[mhf Check Message:0]"); + return response; + } + + return await WebResponse.NotFound(); + } + } +} diff --git a/MonsterHunterFrontierZ.sln b/MonsterHunterFrontierZ.sln new file mode 100644 index 0000000..2cc876e --- /dev/null +++ b/MonsterHunterFrontierZ.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mhf.Server", "Mhf.Server/Mhf.Server.csproj", "{5C0F18BB-B735-4284-82A2-27506DFC9E56}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mhf.Cli", "Mhf.Cli\Mhf.Cli.csproj", "{72B2D640-DEA0-4BA7-A3B5-899954BFBBF5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5C0F18BB-B735-4284-82A2-27506DFC9E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C0F18BB-B735-4284-82A2-27506DFC9E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C0F18BB-B735-4284-82A2-27506DFC9E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C0F18BB-B735-4284-82A2-27506DFC9E56}.Release|Any CPU.Build.0 = Release|Any CPU + {72B2D640-DEA0-4BA7-A3B5-899954BFBBF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72B2D640-DEA0-4BA7-A3B5-899954BFBBF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B2D640-DEA0-4BA7-A3B5-899954BFBBF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72B2D640-DEA0-4BA7-A3B5-899954BFBBF5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..e35a4ab --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +Monster Hunter Frontier Z - Server +=== +Server Emulator for the Online Game Monster Hunter Frontier Z. +Please read through the whole file before asking questions. + +## Table of contents +- [Disclaimer](#disclaimer) +- [Notice](#notice) +- [Setup](#setup) + - [Visual Studio](#visual-studio) + - [VS Code](#vs-code) + - [IntelliJ Rider](#intellij-rider) +- [Hosts](#hosts) +- [Sever](#server) +- [Client](#client) + - [Unpacking](#unpacking) + - [GameGuard](#gameguard) + - [Bugs](#bugs) +- [Guidelines](#guidelines) +- [Attribution](#attribution) + - [Contributers](#contributers) + - [3rd Parties and Libraries](#3rd-parties-and-libraries) + +# Disclaimer +The project is intended for educational purpose only. + +# Notice +This server requires that you own a copy of the game and assets. +These assets are not included in this repository to comply with copyrights. +In order to run this server you are required to provide a `wwww` folder. +At the moment no alternative as been developed, as soon as this happened this repository will be updated. + +# Setup +## 1) Clone the repository +`git clone https://github.com/sebastian-heinz/mhf-server.git` + +## 2) Install .Net Core 3.0 SDK or later +https://dotnet.microsoft.com/download + +## 3) Use your IDE of choice: + +## 3.1) Visual Studio +### Notice: +Minimum version of "Visual Studio 2019 v16.3" or later. + +### Open Project: +Open the `MonsterHunterFrontierZ.sln`-file + +## 3.2) VS Code +Download IDE: https://code.visualstudio.com/download +C# Plugin: https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp + +### Open Project: +Open the Project Folder: +`\mhf-server` + +## 3.3) IntelliJ Rider +https://www.jetbrains.com/rider/ + +### Open Project: +Open the `MonsterHunterFrontierZ.sln`-file + +## 4) Debug the Project +Run the `Mhf.Cli`-Project + +# Hosts +Add following entries to your hosts file to force the client to connect to the local instance. +``` +127.0.0.1 cog-members.mhf-z.jp # MHF Launcher (web) +127.0.0.1 capcom-onlinegames.jp # MHF Authentication (web) +127.0.0.1 www.capcom-onlinegames.jp # MHF Authentication (web) +127.0.0.1 sign-mhf.capcom-networks.jp # MHF Authentication (auth/server) +127.0.0.1 srv-mhf.capcom-networks.jp # MHF ServerList (web) +127.0.0.1 l0.mhf-g.jp # MHF File Checksum (web) +127.0.0.1 u0.mhf-g.jp # MHF File Host (web) +``` + +# Server +With default configuration the server will listen on following ports: +``` +80 - http/launcher gui +433 - https/authentication +53312 - tcp/authentication +53310 - tcp/lobby +``` +ensure that no other local services run on these.request + +# Client +Following modifications are recommended when trying to use this server: + +## Unpacking +mhf.exe, mhl.dll, mhfo.dll and mhfo-hd.dll are protected with AsProtect. +A tool called `AsDecom` can unpack these files, but it must run on WindowsXP. + +## GameGuard +To disable GameGuard please delete or rename `gameguard.des`-file. +Additionally in `mhl.dll` the following byte need to be patched `000053C3:74->77` + +## Bugs +`mhl.dll`-file contains a bug when performing HTTP requests. +``` +00356352 48 54 54 50 2F 31 2E 31 0A 43 HTTP/1.1.C +00356368 61 63 68 65 2D 43 6F 6E 74 72 6F 6C 3A 20 6E 6F ache-Control: no +00356384 2D 63 61 63 68 65 0A -cache. +``` +It only uses a single LF `0x0A` whereas the spec requires CR LF `0x0D 0x0A` +In order to obtain enough bytes we can change `cache-control`-header to `Expires`-headers like so: +``` +Offset(d) 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 + +00356352 48 54 54 50 2F 31 2E 31 0D 0A HTTP/1.1.. +00356368 45 78 70 69 72 65 73 3A 20 30 0D 0A Expires: 0.. +``` +without this patch the C# web server will not process the request, and maybe other servers as well. + +# Guidelines +## Git +### Workflow +The work on this project should happen via `feature-branches` + +Feature branches (or sometimes called topic branches) are used to develop new features for the upcoming or a distant future release. +When starting development of a feature, the target release in which this feature will be incorporated may well be unknown at that point. +The essence of a feature branch is that it exists as long as the feature is in development, +but will eventually be merged back into develop (to definitely add the new feature to the upcoming release) or discarded (in case of a disappointing experiment). + +1) Create a new `feature/feature-name` or `fix/bug-fix-name` branch from master +2) Push all your changes to that branch +3) Create a Pull Request to merge that branch into `master` + +## Best Practise +- Do not use Console.WriteLine etc, use the specially designed logger. +- Own the Code: extract solutions, discard libraries. +- Annotate functions with documentation comments (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments). + +## C# Coding Standards and Naming Conventions +| Object Name | Notation | Char Mask | Underscores | +|:--------------------------|:------------|:-------------------|:------------| +| Class name | PascalCase | [A-z][0-9] | No | +| Constructor name | PascalCase | [A-z][0-9] | No | +| Method name | PascalCase | [A-z][0-9] | No | +| Method arguments | camelCase | [A-z][0-9] | No | +| Local variables | camelCase | [A-z][0-9] | No | +| Constants name | PascalCase | [A-z][0-9] | No | +| Field name | _camelCase | [A-z][0-9] | Yes | +| Properties name | PascalCase | [A-z][0-9] | No | +| Delegate name | PascalCase | [A-z] | No | +| Enum type name | PascalCase | [A-z] | No | + +# Attribution +## Contributors +- Nothilvien [@sebastian-heinz](https://github.com/sebastian-heinz) + +## 3rd Parties and Libraries +- System.Data.SQLite (https://system.data.sqlite.org/) +- MySqlConnector (https://www.nuget.org/packages/MySqlConnector) +- bcrypt.net (https://github.com/BcryptNet/bcrypt.net) +- AspNetCore (https://github.com/aspnet/AspNetCore) +- .NET Standard (https://github.com/dotnet/standard) +- Arrowgene.Services (https://github.com/Arrowgene/Arrowgene.Services) +- CRC32 Implementation by Damien Guard (https://github.com/damieng/DamienGKit/blob/master/CSharp/DamienG.Library/Security/Cryptography/Crc32.cs) + + diff --git a/ReleaseFiles/StartServer.cmd b/ReleaseFiles/StartServer.cmd new file mode 100644 index 0000000..6bc478f --- /dev/null +++ b/ReleaseFiles/StartServer.cmd @@ -0,0 +1,3 @@ +pushd "%~dp0" +cd ./Server. +start Mhf.Cli.exe \ No newline at end of file diff --git a/mhf.version b/mhf.version new file mode 100644 index 0000000..945273a --- /dev/null +++ b/mhf.version @@ -0,0 +1 @@ +1.00 \ No newline at end of file