From b84d49d9f7366dd186140202674ba0874999e281 Mon Sep 17 00:00:00 2001 From: Mikko Rajala <19@teragrep.com> Date: Tue, 15 Feb 2022 11:46:37 +0200 Subject: [PATCH] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 31 + .github/ISSUE_TEMPLATE/config.yml | 11 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/ISSUE_TEMPLATE/tasks-and-meta.md | 11 + .github/workflows/ci.yaml | 51 ++ .github/workflows/upload_release.yaml | 65 ++ .gitignore | 3 + Dockerfile | 8 + LICENSE | 687 ++++++++++++++++++ README.adoc | 93 +++ config/application.properties | 11 + entrypoint.sh | 11 + pom.xml | 328 +++++++++ rpm/rpm.pom.xml | 133 ++++ run-client.sh | 3 + run-server.sh | 2 + .../java/com/teragrep/cfe_16/AckManager.java | 468 ++++++++++++ .../com/teragrep/cfe_16/Cfe16Application.java | 64 ++ .../java/com/teragrep/cfe_16/Converter.java | 183 +++++ .../com/teragrep/cfe_16/EventManager.java | 330 +++++++++ .../java/com/teragrep/cfe_16/LifeCycle.java | 62 ++ .../teragrep/cfe_16/RequestBodyCleaner.java | 82 +++ .../com/teragrep/cfe_16/RequestHandler.java | 90 +++ .../com/teragrep/cfe_16/SessionManager.java | 177 +++++ .../java/com/teragrep/cfe_16/TestClient.java | 425 +++++++++++ .../SyslogMessageSender.java | 289 ++++++++ .../com/teragrep/cfe_16/TokenManager.java | 119 +++ src/main/java/com/teragrep/cfe_16/bo/Ack.java | 115 +++ .../com/teragrep/cfe_16/bo/HeaderInfo.java | 89 +++ .../com/teragrep/cfe_16/bo/HttpEventData.java | 132 ++++ .../java/com/teragrep/cfe_16/bo/Session.java | 147 ++++ .../teragrep/cfe_16/config/Configuration.java | 151 ++++ .../AuthenticationTokenMissingException.java | 73 ++ .../ChannelNotFoundException.java | 73 ++ .../ChannelNotProvidedException.java | 73 ++ .../EventFieldBlankException.java | 73 ++ .../EventFieldMissingException.java | 73 ++ .../exceptionhandling/HECErrorResponse.java | 92 +++ .../HECExceptionHandler.java | 129 ++++ .../InternalServerErrorException.java | 80 ++ .../ServerIsBusyException.java | 73 ++ .../SessionNotFoundException.java | 73 ++ .../cfe_16/rest/HECRestController.java | 250 +++++++ .../cfe_16/sender/AbstractSender.java | 85 +++ .../teragrep/cfe_16/sender/RelpSender.java | 162 +++++ .../teragrep/cfe_16/sender/SenderFactory.java | 68 ++ .../com/teragrep/cfe_16/sender/TcpSender.java | 91 +++ .../com/teragrep/cfe_16/sender/UdpSender.java | 86 +++ .../teragrep/cfe_16/service/HECService.java | 87 +++ .../cfe_16/service/HECServiceImpl.java | 197 +++++ .../resources/application.properties.example | 11 + src/main/resources/control.sh | 159 ++++ src/main/resources/log4j2.xml | 20 + .../cfe_16/Cfe16ApplicationTests.java | 56 ++ .../cfe_16/CleanRequestBodyTests.java | 61 ++ .../com/teragrep/cfe_16/ConverterTests.java | 371 ++++++++++ .../teragrep/cfe_16/SessionManagerTests.java | 105 +++ .../teragrep/cfe_16/TokenManagerTests.java | 113 +++ .../com/teragrep/cfe_16/it/AckManagerIT.java | 311 ++++++++ .../teragrep/cfe_16/it/ConfigurationIT.java | 104 +++ .../com/teragrep/cfe_16/it/SendEventsIT.java | 182 +++++ .../cfe_16/it/ServiceAndEventManagerIT.java | 525 +++++++++++++ src/test/resources/cfe-16-test-plan.jmx | 303 ++++++++ statistics.ods | Bin 0 -> 20883 bytes 64 files changed, 8550 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/tasks-and-meta.md create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/upload_release.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.adoc create mode 100644 config/application.properties create mode 100644 entrypoint.sh create mode 100644 pom.xml create mode 100644 rpm/rpm.pom.xml create mode 100755 run-client.sh create mode 100755 run-server.sh create mode 100644 src/main/java/com/teragrep/cfe_16/AckManager.java create mode 100644 src/main/java/com/teragrep/cfe_16/Cfe16Application.java create mode 100644 src/main/java/com/teragrep/cfe_16/Converter.java create mode 100644 src/main/java/com/teragrep/cfe_16/EventManager.java create mode 100644 src/main/java/com/teragrep/cfe_16/LifeCycle.java create mode 100644 src/main/java/com/teragrep/cfe_16/RequestBodyCleaner.java create mode 100644 src/main/java/com/teragrep/cfe_16/RequestHandler.java create mode 100644 src/main/java/com/teragrep/cfe_16/SessionManager.java create mode 100644 src/main/java/com/teragrep/cfe_16/TestClient.java create mode 100644 src/main/java/com/teragrep/cfe_16/ThirdParty/SyslogMessageSender/SyslogMessageSender.java create mode 100644 src/main/java/com/teragrep/cfe_16/TokenManager.java create mode 100644 src/main/java/com/teragrep/cfe_16/bo/Ack.java create mode 100644 src/main/java/com/teragrep/cfe_16/bo/HeaderInfo.java create mode 100644 src/main/java/com/teragrep/cfe_16/bo/HttpEventData.java create mode 100644 src/main/java/com/teragrep/cfe_16/bo/Session.java create mode 100644 src/main/java/com/teragrep/cfe_16/config/Configuration.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/AuthenticationTokenMissingException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotFoundException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotProvidedException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldBlankException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldMissingException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/HECErrorResponse.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/HECExceptionHandler.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/InternalServerErrorException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/ServerIsBusyException.java create mode 100644 src/main/java/com/teragrep/cfe_16/exceptionhandling/SessionNotFoundException.java create mode 100644 src/main/java/com/teragrep/cfe_16/rest/HECRestController.java create mode 100644 src/main/java/com/teragrep/cfe_16/sender/AbstractSender.java create mode 100644 src/main/java/com/teragrep/cfe_16/sender/RelpSender.java create mode 100644 src/main/java/com/teragrep/cfe_16/sender/SenderFactory.java create mode 100644 src/main/java/com/teragrep/cfe_16/sender/TcpSender.java create mode 100644 src/main/java/com/teragrep/cfe_16/sender/UdpSender.java create mode 100644 src/main/java/com/teragrep/cfe_16/service/HECService.java create mode 100644 src/main/java/com/teragrep/cfe_16/service/HECServiceImpl.java create mode 100644 src/main/resources/application.properties.example create mode 100755 src/main/resources/control.sh create mode 100644 src/main/resources/log4j2.xml create mode 100644 src/test/java/com/teragrep/cfe_16/Cfe16ApplicationTests.java create mode 100644 src/test/java/com/teragrep/cfe_16/CleanRequestBodyTests.java create mode 100644 src/test/java/com/teragrep/cfe_16/ConverterTests.java create mode 100644 src/test/java/com/teragrep/cfe_16/SessionManagerTests.java create mode 100644 src/test/java/com/teragrep/cfe_16/TokenManagerTests.java create mode 100644 src/test/java/com/teragrep/cfe_16/it/AckManagerIT.java create mode 100644 src/test/java/com/teragrep/cfe_16/it/ConfigurationIT.java create mode 100644 src/test/java/com/teragrep/cfe_16/it/SendEventsIT.java create mode 100644 src/test/java/com/teragrep/cfe_16/it/ServiceAndEventManagerIT.java create mode 100644 src/test/resources/cfe-16-test-plan.jmx create mode 100644 statistics.ods diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..19da528 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** + + +**Expected behavior** + + +**How to reproduce** + + +**Screenshots** + + +**Software version** + + +**Desktop (please complete the following information if relevant):** + - OS: + - Browser: + - Version: + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3363fc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Doc issue report + url: https://github.com/teragrep/doc_01/issues/new?template=doc-issue-report.md + about: Problems with Teragrep documentation + - name: Ask a question or get support + url: https://github.com/teragrep/cfe_16/discussions + about: Ask a question or request support + - name: Report vulnerability + url: https://github.com/teragrep/teragrep/security/advisories/new + about: Privately report a security vulnerability diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..9baeccc --- /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: enhancement +assignees: '' + +--- + +**Description** + + +**Use case or motivation behind the feature request** + + +**Related issues** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/tasks-and-meta.md b/.github/ISSUE_TEMPLATE/tasks-and-meta.md new file mode 100644 index 0000000..17546e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tasks-and-meta.md @@ -0,0 +1,11 @@ +--- +name: Tasks and meta +about: Maintainers only +title: '' +labels: '' +assignees: '' + +--- + +**Description** + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8036b86 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,51 @@ +name: CI + +on: push + +jobs: + verify: + name: Verify Code + runs-on: ubuntu-latest + + env: + COVERITY: coverity_tool + + steps: + - uses: actions/checkout@v3 + + - name: Setup Maven Central + uses: actions/setup-java@v3 + with: + java-version: 8.0.292+10 + distribution: 'adopt' + + - name: Cache Local Maven Repository + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: Compile Test and Verify + run: mvn --batch-mode clean verify + + - name: Cache Coverity + id: cache_coverity + uses: actions/cache@v3 + with: + path: ${{ env.COVERITY }} + key: coverity + + - name: Download Coverity + if: steps.cache_coverity.outputs.cache-hit != 'true' + run: | + wget --quiet https://scan.coverity.com/download/linux64 --post-data "token=${{ secrets.COVERITY_TOKEN }}&project=cfe_16" -O ${{ env.COVERITY }}.tgz + mkdir -p ${{ env.COVERITY }} + tar zxvf ${{ env.COVERITY }}.tgz -C ${{ env.COVERITY }} --strip-components 1 + + - name: Compile Coverity + run: | + ${{ env.COVERITY }}/bin/cov-build --dir cov-int mvn -DskipTests=true --batch-mode clean compile + tar czvf cfe_16.tgz cov-int + + - name: Upload to Coverity + run: curl --silent --form token=${{ secrets.COVERITY_TOKEN }} --form email=${{ secrets.COVERITY_EMAIL }} --form file=@cfe_16.tgz --form version="${GITHUB_REF##*/}" --form description="automated upload" https://scan.coverity.com/builds?project=cfe_16 diff --git a/.github/workflows/upload_release.yaml b/.github/workflows/upload_release.yaml new file mode 100644 index 0000000..2d1b219 --- /dev/null +++ b/.github/workflows/upload_release.yaml @@ -0,0 +1,65 @@ +name: Upload Release + +on: + release: + types: published + +jobs: + upload: + name: Upload + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Cache Local Maven Repository + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: Setup GitHub Packages + uses: actions/setup-java@v3 + with: + java-version: 8.0.292+10 + distribution: 'adopt' + + - name: Get version + run: git describe --tags && echo "RELEASE_VERSION=$(git describe --tags)" >> $GITHUB_ENV + + - name: Create jar and rpm + run: | + mvn -B -Drevision=${{ env.RELEASE_VERSION }} -Dsha1= -Dchangelist= clean verify deploy; + cd rpm/ && mvn -B -Drevision=${{ env.RELEASE_VERSION }} -Dsha1= -Dchangelist= -f rpm.pom.xml package; + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Attach rpm as artifact on releases + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: target/rpm/com.teragrep-cfe_16/RPMS/noarch/com.teragrep-cfe_16-*.rpm + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase repository name + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: 'Build Image' + run: | + docker buildx build --output type=docker --tag ghcr.io/${{ env.REPO_LC }}/app:${{ github.event.release.tag_name }} . + docker push ghcr.io/${{ env.REPO_LC }}/app:${{ github.event.release.tag_name }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e7ba63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +target/ +rpm/target/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da35738 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM rockylinux:9 +COPY rpm/target/rpm/com.teragrep-cfe_16/RPMS/noarch/com.teragrep-cfe_16-*.rpm /rpm/ +RUN yum -y localinstall /rpm/*.rpm && yum clean all + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +USER srv-cfe_16 +ENTRYPOINT /entrypoint.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05587a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,687 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. + +Additional permission under GNU Affero General Public License version 3 +section 7 + +If you modify this Program, or any covered work, by linking or combining it with +other code, such other code is not for that reason alone subject to any of the +requirements of the GNU Affero GPL version 3 as long as this Program is the same +Program as licensed from Suomen Kanuuna Oy without any additional modifications. + +Supplemented terms under GNU Affero General Public License version 3 +section 7 + +Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified +versions must be marked as "Modified version of" The Program. + +Names of the licensors and authors may not be used for publicity purposes. + +No rights are granted for use of trade names, trademarks, or service marks +which are in The Program if any. + +Licensee must indemnify licensors and authors for any liability that these +contractual assumptions impose on licensors and authors. + +To the extent this program is licensed as part of the Commercial versions of +Teragrep, the applicable Commercial License may apply to this file if you as +a licensee so wish it. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..9c5cfb9 --- /dev/null +++ b/README.adoc @@ -0,0 +1,93 @@ +:opening-bracket: [ +:closing-bracket: ] + += HTTP Event Capture, HEC, to Syslog +[![Build Status](https://scan.coverity.com/projects/24515/badge.svg)](https://scan.coverity.com/projects/cfe_16) + +== License +AGPLv3 with link:https://github.com/teragrep/cfe_16/blob/master/LICENSE#L665-L670[additional permissions] granted in the license. + +== Compiling + +[source, shell script] +---- +mvn clean verify package +---- + +== Running + +=== Configuration + +Default properties location is at config/application.properties + +Supported protocols are RELP, TCP, UDP. + +[source, properties] +---- +syslog.server.host=127.0.0.1 +syslog.server.port=601 +syslog.server.protocol=RELP +max.channels=1000000 +max.ack.value=1000000 +max.ack.age=20000 +max.session.age=30000 +poll.time=300000 +config.poll.time=5000 +spring.devtools.add-properties=false +server.print.times=true +---- + +NOTE: It is advised to use RELP and rsyslog for reception for data durability. + +=== Execution + +[source, shell script] +---- +java -jar target/cfe_16.jar +---- + +== Testing + +=== Performance +Performance test client execution + +Test client is located in class com.teragrep.cfe_16.TestClient + +TestClient parameters: + +. cfe_16 server hostname +. cfe_16 server port +. number of threads +. number of loops per thread + +Connect to Spring embedded Tomcat at localhost:8080, and instantiate one thread +doing the HTTP requests. + +[source, shell script] +---- +java -classpath target/classes com.teragrep.cfe_16.TestClient localhost 8080 1 4 +---- + +NOTE: It is advised to warm up the JVM before reporting the results by running +a warm-up loop with intended amount of threads and 50 loops per thread. + +== Contributing + +// Change the repository name in the issues link to match with your project's name + +You can involve yourself with our project by https://github.com/teragrep/cfe_16/issues/new/choose[opening an issue] or submitting a pull request. + +Contribution requirements: + +. *All changes must be accompanied by a new or changed test.* If you think testing is not required in your pull request, include a sufficient explanation as why you think so. +. Security checks must pass +. Pull requests must align with the principles and http://www.extremeprogramming.org/values.html[values] of extreme programming. +. Pull requests must follow the principles of Object Thinking and Elegant Objects (EO). + +Read more in our https://github.com/teragrep/teragrep/blob/main/contributing.adoc[Contributing Guideline]. + +=== Contributor License Agreement + +Contributors must sign https://github.com/teragrep/teragrep/blob/main/cla.adoc[Teragrep Contributor License Agreement] before a pull request is accepted to organization's repositories. + +You need to submit the CLA only once. After submitting the CLA you can contribute to all Teragrep's repositories. diff --git a/config/application.properties b/config/application.properties new file mode 100644 index 0000000..f609186 --- /dev/null +++ b/config/application.properties @@ -0,0 +1,11 @@ +syslog.server.host=127.0.0.1 +syslog.server.port=1235 +syslog.server.protocol=relp +max.channels=1000000 +max.ack.value=1000000 +max.ack.age=20000 +max.session.age=30000 +poll.time=300000 +config.poll.time=5000 +spring.devtools.add-properties=false +server.print.times=true diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..066a61e --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [ -z ${XMS} ]; then + XMS=256m +fi +if [ -z ${XMX} ]; then + XMX=512m +fi +echo "Xms=${XMS}" +echo "Xmx=${XMX}" +java -jar /opt/teragrep/cfe_16/lib/cfe_16.jar -Xms${XMS} -Xmx${XMX} -Dspring.config.location="file://${CONFIG_PATH}" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4195e19 --- /dev/null +++ b/pom.xml @@ -0,0 +1,328 @@ + + + jar + 4.0.0 + cfe_16 + ${revision}${sha1}${changelist} + cfe_16 + cfe_16 + com.teragrep + + UTF-8 + 1.8 + ${java.version} + ${java.version} + 0.0.1 + -SNAPSHOT + + 1.9.21.1 + 2.23.1 + 2.7.18 + 2.7.4 + 5.10.2 + 1.10.2 + 4.0.1 + 3.0.0 + 4.0.1 + 2.16.2 + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + + javax.servlet + javax.servlet-api + ${javax.servlet.version} + + + com.teragrep + rlp_01 + ${rlp_01.version} + + + com.teragrep + rlp_03 + ${rlp_03.version} + test + + + org.springframework.boot + spring-boot-starter + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring.boot.version} + + + org.springframework.boot + spring-boot-devtools + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-actuator + ${spring.boot.version} + + + org.springframework.session + spring-session-core + ${spring.session.version} + + + org.springframework.session + spring-session-jdbc + ${spring.session.version} + + + org.aspectj + aspectjrt + ${aspectj.version} + provided + + + org.aspectj + aspectjweaver + ${aspectj.version} + provided + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.cloudbees + syslog-java-client + 1.1.7 + + + com.google.code.gson + gson + 2.10.1 + + + systems.manifold + manifold-all + 2024.1.6 + + + org.powermock + powermock-api-mockito2 + 2.0.9 + test + + + org.apache.felix + maven-bundle-plugin + 5.1.9 + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.platform + junit-platform-commons + ${junit.platform.version} + test + + + + ${project.basedir}/target + cfe_16 + + + maven-enforcer-plugin + 3.4.1 + + + enforce + none + + + enforce-maven + + enforce + + + + + 3.2.5 + + + 1.8 + + + + + + + + org.apache.rat + apache-rat-plugin + 0.15 + false + + + test + + check + + + + + false + + + Also allow the license url to be https. + + Copyright (C) 2021 Suomen Kanuuna Oy + + + + true + false + + + .git/** + .gitattributes + .gitignore + .gitmodules + + .github/workflows/*.yml + .github/workflows/*.yaml + .github/ISSUE_TEMPLATE/* + toolchains.xml + settings.xml + rpm/rpm.pom.xml + + README.adoc + README.md + + src/main/java/com/teragrep/cfe_16/ThirdParty/SyslogMessageSender/SyslogMessageSender.java + + src/main/resources/* + src/test/resources/cfe-16-test-plan.jmx + + rpm/rpm.pom.xml + Dockerfile + config/* + *.sh + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + com.teragrep.cfe_16.Cfe16Application + ${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar + + + + + repackage + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + 1 + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + ${project.build.outputDirectory} + + + + integration-tests + + integration-test + + + + verify + + verify + + + + + + + + + github + https://maven.pkg.github.com/${env.GITHUB_REPOSITORY} + + + diff --git a/rpm/rpm.pom.xml b/rpm/rpm.pom.xml new file mode 100644 index 0000000..7ce9c39 --- /dev/null +++ b/rpm/rpm.pom.xml @@ -0,0 +1,133 @@ + + + rpm + 4.0.0 + cfe_16 + ${revision}${sha1}${changelist} + cfe_16 + cfe_16 + com.teragrep + https://teragrep.com + + UTF-8 + 1.8 + 1.8 + 1.8 + 0.0.1 + -SNAPSHOT + + + + scm:git:https://github.com/teragrep/cfe_16.git + scm:git:git@github.com:teragrep/cfe_16.git + https://github.com/teragrep/cfe_16/tree/master + + + ${project.basedir}/target + + + maven-enforcer-plugin + 3.4.1 + + + enforce + none + + + enforce-maven + + enforce + + + + + 3.2.5 + + + 1.8 + + + + + + + + org.codehaus.mojo + rpm-maven-plugin + 2.2.0 + true + + + default-rpm + + rpm + + package + + + + ${project.groupId}-${project.artifactId} + ${project.groupId}-${project.artifactId} + ${project.version} + ${env.GITHUB_RUN_NUMBER} + Proprietary + Fail-Safe Log Management Suite + https://fail-safe.net/ + Fail-Safe <servicedesk@fail-safe.net> + Fail-Safe/LogManagementSuite + ${project.groupId}-${project.artifactId} + false + srv-cfe_16 + srv-cfe_16 + 0644 + 0755 + + _build_id_links none + + + + /opt/teragrep/${project.artifactId}/lib + true + 755 + 755 + srv-cfe_16 + srv-cfe_16 + true + + + ${project.basedir}/../target/cfe_16.jar + + + + + + java-1.8.0-openjdk + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + true + + + + + + + github + https://maven.pkg.github.com/${env.GITHUB_REPOSITORY} + + + diff --git a/run-client.sh b/run-client.sh new file mode 100755 index 0000000..7cbdd9b --- /dev/null +++ b/run-client.sh @@ -0,0 +1,3 @@ +#!/bin/bash +java -classpath target/classes comt.teragrep.cfe_16.TestClient $1 $2 $3 $4 + diff --git a/run-server.sh b/run-server.sh new file mode 100755 index 0000000..0356167 --- /dev/null +++ b/run-server.sh @@ -0,0 +1,2 @@ +mvn spring-boot:run -Dspring.config.location="file://src/main/resources/application.properties" + diff --git a/src/main/java/com/teragrep/cfe_16/AckManager.java b/src/main/java/com/teragrep/cfe_16/AckManager.java new file mode 100644 index 0000000..ec9726b --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/AckManager.java @@ -0,0 +1,468 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.teragrep.cfe_16.bo.Ack; +import com.teragrep.cfe_16.config.Configuration; +import com.teragrep.cfe_16.exceptionhandling.InternalServerErrorException; +import com.teragrep.cfe_16.exceptionhandling.ServerIsBusyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/* + * Manager that handles the acknowledgement status of the sent events (acks). + * A background thread is used to clean up NRU ACK objects. + * + * This class is thread safe. + * + */ +@Component +public class AckManager implements Runnable, LifeCycle { + private static final Logger LOGGER = LoggerFactory.getLogger(AckManager.class); + /** + * A class that encapsulates state of individual channels regarding to ACKs. + * + */ + private class State { + private int currentAckValue; + private Ack ackToCompare; + private Map ackMap; + + public State() { + this.currentAckValue = 0; + this.ackToCompare = new Ack(); + this.ackMap = new HashMap(); + } + + public int getCurrentAckValue() { + return this.currentAckValue; + } + + public void setCurrentAckValue(int currentAckValue) { + this.currentAckValue = currentAckValue; + } + + public Ack getAckToCompare() { + return this.ackToCompare; + } + + public void setAckToCompare(Ack ackToCompare) { + this.ackToCompare = ackToCompare; + } + + public Map getAckMap() { + return this.ackMap; + } + + @Override + public String toString() { + return "State [currentAckValue=" + this.currentAckValue + ", ackToCompare=" + this.ackToCompare + + ", ackMap=" + this.ackMap + "]"; + } + } + + /** + * Does the JSON <-> Java conversions. + */ + private final ObjectMapper objectMapper; + + /** + * A hash for mapping channels to ACK status objects. + */ + private final Map ackStates; + + /** + * The background thread for cleaning up ACKs. + */ + private Thread cleanerThread; + + @Autowired + private Configuration configuration; + + /** + * An empty constructor for Spring @Autowired annotation. + */ + public AckManager() { + this.objectMapper = new ObjectMapper(); + this.ackStates = Collections.synchronizedMap(new HashMap()); + } + + @Override + @PostConstruct + public void start() { + this.cleanerThread = new Thread(this); + this.cleanerThread.start(); + } + + /** + * Let's interrupt the cleaner thread from its eternal run(). + */ + @Override + public void stop() { + this.cleanerThread.interrupt(); + } + + /** + * A private Accessor for the State object indexed by the given auth token and channel. + * If no State object is found, a new object is created and added to the map. + * + * @param authToken + * @param channel + * @return + */ + private State getOrCreateState(String authToken, String channel) { + LOGGER.debug("Getting or creating state for channel <{}>", channel); + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + state = new State(); + this.ackStates.put(key, state); + } + LOGGER.debug("Created state <{}> for channel <{}>", state, channel); + return state; + } + + /** + * This method has to be called first before calling any other Ack related + * methods. + * + * @param authToken + * @param channel + */ + public void initializeContext(String authToken, String channel) { + LOGGER.debug("Initializing context for channel <{}>", channel); + String key = authToken + channel; + if (!this.ackStates.containsKey(key)) { + LOGGER.debug("Adding new state to channel <{}>", channel); + State state = new State(); + this.ackStates.put(key, state); + } + } + + /* + * Assignes an Ack value for the event. Checks it there are acks still available + * for the channel If there are no Acks available, throws ServerIsBusyException. + * Uses the ackToCompare variable to check if Ack with a current value is in the + * Ack list and increases the Ack value until a suitable Ack value is found and + * when it is found, a new Ack with the current Ack value as an id is created. + */ + public boolean incrementAckValue(String authToken, String channel) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + return false; + } + + if (!acksAvailable(state)) { + throw new ServerIsBusyException(); + } + + int currentAckValue; + synchronized (state) { + currentAckValue = state.getCurrentAckValue(); + Ack ackToCompare = state.getAckToCompare(); + ackToCompare.setId(currentAckValue); + state.setAckToCompare(ackToCompare); + Map ackMap = state.getAckMap(); + while (ackMap.containsKey(ackToCompare.getId())) { + currentAckValue++; + ackToCompare = state.getAckToCompare(); + ackToCompare.setId(currentAckValue); + if (currentAckValue > this.configuration.getMaxAckValue()) { + currentAckValue = 0; + } + state.setAckToCompare(ackToCompare); + } + + currentAckValue++; + if (currentAckValue > this.configuration.getMaxAckValue()) { + currentAckValue = 0; + } + state.setCurrentAckValue(currentAckValue); + } + return true; + } + + /* + * Uses the help Ack object to find the Ack object from the Ack set and sets the + * acknowledgement status of the Ack as true. + */ + public boolean acknowledge(String authToken, String channel, int ackId) { + String key = authToken + channel; + State state = this.ackStates.get(key); + LOGGER.debug("Acknowledging ackId <{}> on channel <{}>", ackId, channel); + if (state == null) { + throw new IllegalStateException("An Ack cannot be acknowledge before it is added to the Ack list."); + } + synchronized (state) { + Map ackMap = state.getAckMap(); + Ack ack = ackMap.get(ackId); + if (ack == null) { + throw new InternalServerErrorException("Couldn't set the acknowledge status for Ack ID " + ackId); + } + ack.acknowledge(); + return true; + } + } + + /** + * Adds a new Ack object for given channel. If this is the first time a channel + * is assigned a new Ack, a new State object is created. + * + * @param channel + * @param ack + */ + public boolean addAck(String authToken, String channel, Ack ack) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + throw new InternalServerErrorException("No State for key " + key); + } + synchronized (state) { + state.ackMap.put(ack.getId(), ack); + return true; + } + } + + /** + * Checks if the Ack with a given id is acknowledged. + */ + public boolean isAckAcknowledged(String authToken, String channel, int ackId) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + throw new InternalServerErrorException("No State for key " + key); + } + synchronized (state) { + Map ackMap = state.getAckMap(); + Ack ack = ackMap.get(ackId); + if (ack == null) { + return false; + } + ack.acknowledge(); + return true; + } + } + + /** + * Returns the Ack statuses of requested Ack id:s as a JSON node. JSON node with + * the id:s is given as a parameter. Example: {"acks": [1,3,4]} + */ + public JsonNode getRequestedAckStatuses(String authToken, String channel, JsonNode requestedAcksInJson) { + JsonNode jsonNode = this.objectMapper.createObjectNode(); + if (requestedAcksInJson == null) { + return jsonNode; + } + + /* + * Checks that the JSON parameter is given, and that there is an Ack node which + * is an array. Saves the requested ack ids in an int array. + */ + int[] requestedAckIds = null; + if (requestedAcksInJson.get("acks") != null && requestedAcksInJson.get("acks").isArray()) { + requestedAckIds = this.objectMapper.convertValue(requestedAcksInJson.get("acks"), int[].class); + } + + /* + * Goes through the requested Ack id:s, checks if they have been assigned to an + * Ack object in the Ack list. If an Ack is found, the Ack id and the + * acknowledgement status of that Ack is saved in "ackStatuses" JSON node. If + * Ack is not found in the list, the Ack id and acknowledgement status "false" + * is added to the JSON node. + */ + + Map ackStatuses = new HashMap(); + if (requestedAckIds != null) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + return null; + } + synchronized (state) { + Map ackMap = state.getAckMap(); + for (int i = 0; i < requestedAckIds.length; i++) { + int ackId = requestedAckIds[i]; + Ack ack = ackMap.get(ackId); + if (ack == null) { + ackStatuses.put(ackId, false); + } else { + ackStatuses.put(ackId, ack.isAcknowledged()); + ackMap.remove(ackId); + } + } + } + } + jsonNode = this.objectMapper.convertValue(ackStatuses, JsonNode.class); + return jsonNode; + } + + /* + * Checks if there is room in the Ack list for a new entry + * + * Not thread safe, needs external synchronization. + */ + private boolean acksAvailable(State state) { + Map ackMap = state.getAckMap(); + int ackMapSize = ackMap.size(); + int maxAckValue = this.configuration.getMaxAckValue(); + if (ackMapSize > maxAckValue) { + return false; + } else { + return true; + } + } + + /* + * Deletes a given Ack from the Ack list. + * + * This is an O(n) operation.. + */ + public boolean deleteAckFromList(String authToken, String channel, Ack ack) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + throw new InternalServerErrorException("No State for key " + key); + } + synchronized (state) { + state.getAckMap().remove(ack.getId()); + return true; + } + } + + public void run() { + + while (true) { + try { + LOGGER.debug("Sleeping for <{}> while waiting for polls", this.configuration.getPollTime()); + Thread.sleep(this.configuration.getPollTime()); + } catch (InterruptedException e) { + break; + } + + for (String key : this.ackStates.keySet()) { + State state = this.ackStates.get(key); + + synchronized (state) { + Map ackMap = state.getAckMap(); + Iterator iterator = ackMap.values().iterator(); + while (iterator.hasNext()) { + Ack ack = iterator.next(); + long thresholdInLong = ack.getLastUsedTimestamp() + this.configuration.getMaxAckAge(); + + /** + * If the Ack object is too old we'll remove it from the Ack set. + */ + long now = System.currentTimeMillis(); + if (now >= thresholdInLong) { + iterator.remove(); + } + } + } + } + } + } + + /** + * Returns the size of the Ack set of given channel. + * + * @param channel + * @return + */ + public int getAckListSize(String authToken, String channel) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + throw new InternalServerErrorException("No State for key " + key); + } + synchronized (state) { + return state.getAckMap().size(); + } + } + + /** + * Returns the unmodifiable set of Acks for a channel. + * + * @param channel + * @return + */ + public Map getAckList(String authToken, String channel) { + String key = authToken + channel; + State state = this.ackStates.get(key); + if (state == null) { + throw new InternalServerErrorException("No State for key " + key); + } + synchronized (state) { + return state.getAckMap(); + } + } + + /** + * Returns the current Ack value for given token and channel. + * A new State is created, so this method must be called + * first before other Ack manipulating methods are called. + * + * @param authToken + * @param channel + * @return + */ + public int getCurrentAckValue(String authToken, String channel) { + State state = this.getOrCreateState(authToken, channel); + synchronized (state) { + return state.getCurrentAckValue(); + } + } +} diff --git a/src/main/java/com/teragrep/cfe_16/Cfe16Application.java b/src/main/java/com/teragrep/cfe_16/Cfe16Application.java new file mode 100644 index 0000000..22cfc2b --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/Cfe16Application.java @@ -0,0 +1,64 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan({ "com.teragrep.cfe_16" }) +public class Cfe16Application { + private static final Logger LOGGER = LoggerFactory.getLogger(Cfe16Application.class); + public static void main(String[] args) { + LOGGER.info("Starting Cfe16Application..."); + SpringApplication.run(Cfe16Application.class, args); + //Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + } +} diff --git a/src/main/java/com/teragrep/cfe_16/Converter.java b/src/main/java/com/teragrep/cfe_16/Converter.java new file mode 100644 index 0000000..0832339 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/Converter.java @@ -0,0 +1,183 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.cloudbees.syslog.Facility; +import com.cloudbees.syslog.SDElement; +import com.cloudbees.syslog.Severity; +import com.cloudbees.syslog.SyslogMessage; +import com.teragrep.cfe_16.bo.HeaderInfo; +import com.teragrep.cfe_16.bo.HttpEventData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/* + * Converts HTTP Event Data into a Syslog message. + * + * This class is NOT thread safe! + * + */ +@Component +public class Converter { + private static final Logger LOGGER = LoggerFactory.getLogger(Converter.class); + private Severity severity; + private Facility facility; + + private SDElement metadataSDE; + private SDElement headerSDE; + + public SyslogMessage httpToSyslog(HttpEventData httpEventData, HeaderInfo headerInfo) { + + setEventSeverity(); + setEventFacility(); + + setStructuredDataParams(httpEventData); + setHeaderSDE(headerInfo); + + SyslogMessage syslogMessage; + if (httpEventData.isTimeParsed()) { + + /* + * Creates a Syslogmessage with a time stamp + */ + LOGGER.debug("Creating new syslog message with timestamp"); + syslogMessage = new SyslogMessage().withTimestamp(httpEventData.getTimeAsLong()).withSeverity(severity) + .withAppName("capsulated").withHostname("cfe_16").withFacility(facility).withSDElement(metadataSDE) + .withSDElement(headerSDE).withMsg(httpEventData.getEvent()); + + } else { + /* + * Creates a Syslogmessage without timestamp, because the time is already given + * in the request. + */ + LOGGER.debug("Creating new syslog message without timestamp"); + syslogMessage = new SyslogMessage().withSeverity(severity).withAppName("capsulated").withHostname("cfe_16") + .withFacility(facility).withSDElement(metadataSDE).withSDElement(headerSDE) + .withMsg(httpEventData.getEvent()); + } + + return syslogMessage; + } + + /* + * Event severity is coded to always be INFORMATIONAL + */ + private void setEventSeverity() { + severity = Severity.INFORMATIONAL; + } + + /* + * Event facility is coded to always be USER + */ + private void setEventFacility() { + facility = Facility.USER; + } + + /* + * Gets the data from the HTTP Event Data and adds it to SD Element as SD + * Parameters. + */ + private void setStructuredDataParams(HttpEventData eventData) { + LOGGER.debug("Setting Structured Data params"); + metadataSDE = new SDElement("cfe_16-metadata@48577"); + + if (eventData.getAuthenticationToken() != null) { + LOGGER.debug("Setting authentication token"); + metadataSDE.addSDParam("authentication_token", eventData.getAuthenticationToken()); + } + + if (eventData.getChannel() != null) { + LOGGER.debug("Setting channel"); + metadataSDE.addSDParam("channel", eventData.getChannel()); + } + + if (eventData.getAckID() != null) { + LOGGER.debug("Setting ack id"); + metadataSDE.addSDParam("ack_id", String.valueOf(eventData.getAckID())); + } + + if (eventData.getTimeSource() != null) { + LOGGER.debug("Setting time source"); + metadataSDE.addSDParam("time_source", eventData.getTimeSource()); + } + + if (eventData.isTimeParsed()) { + LOGGER.debug("Setting time_parsed and time"); + metadataSDE.addSDParam("time_parsed", "true"); + metadataSDE.addSDParam("time", eventData.getTime()); + } + } + + public SyslogMessage getHeaderInfoSyslogMessage(HeaderInfo headerInfo) { + + SyslogMessage syslogMessage = null; + setHeaderSDE(headerInfo); + syslogMessage = new SyslogMessage().withSeverity(severity).withAppName("capsulated").withHostname("cfe_16") + .withFacility(facility).withSDElement(headerSDE); + + return syslogMessage; + } + + private void setHeaderSDE(HeaderInfo headerInfo) { + LOGGER.debug("Setting Structured Data headers"); + headerSDE = new SDElement("cfe_16-origin@48577"); + + if (headerInfo.getxForwardedFor() != null) { + LOGGER.debug("Adding X-Forwarded-For header to headerSDE"); + headerSDE.addSDParam("X-Forwarded-For", headerInfo.getxForwardedFor()); + } + if (headerInfo.getxForwardedHost() != null) { + LOGGER.debug("Adding X-Forwarder-Host to headerSDE"); + headerSDE.addSDParam("X-Forwarded-Host", headerInfo.getxForwardedHost()); + } + if (headerInfo.getxForwardedProto() != null) { + LOGGER.debug("Adding X-Forwarded-Proto to headerSDE"); + headerSDE.addSDParam("X-Forwarded-Proto", headerInfo.getxForwardedProto()); + } + } +} diff --git a/src/main/java/com/teragrep/cfe_16/EventManager.java b/src/main/java/com/teragrep/cfe_16/EventManager.java new file mode 100644 index 0000000..4a435c4 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/EventManager.java @@ -0,0 +1,330 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.cloudbees.syslog.SyslogMessage; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonStreamParser; +import com.teragrep.cfe_16.bo.Ack; +import com.teragrep.cfe_16.bo.HeaderInfo; +import com.teragrep.cfe_16.bo.HttpEventData; +import com.teragrep.cfe_16.bo.Session; +import com.teragrep.cfe_16.config.Configuration; +import com.teragrep.cfe_16.exceptionhandling.EventFieldBlankException; +import com.teragrep.cfe_16.exceptionhandling.EventFieldMissingException; +import com.teragrep.cfe_16.exceptionhandling.InternalServerErrorException; +import com.teragrep.cfe_16.sender.AbstractSender; +import com.teragrep.cfe_16.sender.SenderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/* + * Manager that handles the event sent in a request. + * + */ +@Component +public class EventManager { + private static final Logger LOGGER = LoggerFactory.getLogger(EventManager.class); + private final ObjectMapper objectMapper; + + @Autowired + private Configuration configuration; + + private AbstractSender sender; + + public EventManager() { + this.objectMapper = new ObjectMapper(); + } + + @PostConstruct + public void setupSender() { + LOGGER.debug("Setting up sender"); + try { + this.sender = SenderFactory.createSender(this.configuration.getSysLogProtocol(), + this.configuration.getSyslogHost(), + this.configuration.getSyslogPort()); + } catch (IOException e) { + LOGGER.error("Error creating sender", e); + throw new InternalServerErrorException(); + } + } + + /* + * Method used when converting data and the channel is specified in the request. + * Takes authentication token, all events sent in a request (in JSON format) and + * the channel name as string parameters. Returns a JSON node with ack id if + * everything is successful. Example: {"text":"Success","code":0,"ackID":0} + */ + public ObjectNode convertData(String authToken, + String channel, + String allEventsInJson, + HeaderInfo headerInfo, + AckManager ackManager) { + HttpEventData previousEvent = null; + + ackManager.initializeContext(authToken, channel); + int ackId = ackManager.getCurrentAckValue(authToken, channel); + boolean incremented = ackManager.incrementAckValue(authToken, channel); + if (!incremented) { + throw new InternalServerErrorException("Ack value couldn't be incremented."); + } + Ack ack = new Ack(ackId, false); + boolean addedAck = ackManager.addAck(authToken, channel, ack); + if (!addedAck) { + throw new InternalServerErrorException("Ack ID " + ackId + " couldn't be added to the Ack set."); + } + JsonStreamParser parser = new JsonStreamParser(allEventsInJson); + + /* + * There can be multiple events in one request. Here they are handled one by + * one. The event is saved in a string variable and is converted into + * HttpEventData object. Metadata is assigned to the object. HttpEventData is + * converted into SyslogMessage and saved in a list in a RequestInfo object. + * After the event is handled, it is assigned as a value to previousEvent + * variable. + */ + HttpEventData eventData = null; + Converter converter = new Converter(); + List syslogMessages = new ArrayList(); + while (parser.hasNext()) { + previousEvent = eventData; + String jsonObjectStr = parser.next().toString(); + eventData = verifyJsonData(jsonObjectStr, previousEvent); + eventData = assignMetaData(eventData, authToken, channel); + SyslogMessage syslogMessage = converter.httpToSyslog(eventData, headerInfo); + syslogMessages.add(syslogMessage); + } + + /* + * SyslogMessage syslogMessage = + * converter.getHeaderInfoSyslogMessage(headerInfo); + * requestInfo.getConvertedData().add(syslogMessage); + */ + /* + * After all the events are sent, previousEvent object is set to null, the + * events are sent with the ackManager and ack id and JSON node with an ack id + * will be returned informing that the sending of the events has been + * successful. + */ + previousEvent = null; + + // create a new object to avoid blocking of threads because + // the SyslogMessageSender.sendMessage() is synchronized + try { + SyslogMessage[] messages = syslogMessages.toArray(new SyslogMessage[syslogMessages.size()]); + this.sender.sendMessages(messages); + } catch (IOException e) { + throw new InternalServerErrorException(e); + } + + boolean shouldAck = channel != null && !channel.equals(Session.DEFAULT_CHANNEL); + + if (shouldAck) { + boolean acked = ackManager.acknowledge(authToken, channel, ackId); + if (!acked) { + throw new InternalServerErrorException("Ack ID " + ackId + " not Acked."); + } + } + + ObjectNode responseNode = this.objectMapper.createObjectNode(); + + responseNode.put("text", "Success"); + responseNode.put("code", 0); + if (shouldAck) { + responseNode.put("ackID", ackId); + } + + return responseNode; + } + + /* + * Pre-handles the event and assigns it's information into HttpEventData object + * and returns it. Information from the string is read with ObjectMapper into a + * JsonNode. After that the supposed fields are checked from the JsonNode and if + * the field is found, information from it will be saved in HttpEventData + * object. Finally handleTime() is called to assign correct time information to + * the object. When multiple events are sent in one request, the value of the + * fields are saved for the following events. The values can be overriden. If + * the value is overridden, it will stay as so for the following events if it is + * not overridden. + */ + private HttpEventData verifyJsonData(String eventInJson, HttpEventData previousEvent) { + + HttpEventData eventData = new HttpEventData(); + + /* + * Event field cannot be missing or blank. Throws an exception if this is the + * case. + */ + JsonNode jsonObject; + try { + jsonObject = this.objectMapper.readTree(eventInJson); + } catch (IOException e) { + jsonObject = null; + } + + if (jsonObject != null) { + JsonNode event = jsonObject.get("event"); + if (event != null) { + eventData.setEvent(event.toString()); + } else { + throw new EventFieldMissingException(); + } + if (eventData.getEvent().matches("\"\"") || eventData.getEvent() == null) { + throw new EventFieldBlankException(); + } + eventData = handleTime(eventData, jsonObject, previousEvent); + } + return eventData; + } + + /* + * The time stamp of the event can be given as epoch time in the request. + */ + private HttpEventData handleTime(HttpEventData eventData, JsonNode jsonObject, HttpEventData previousEvent) { + JsonNode timeObject = jsonObject.get("time"); + + /* + * If the time is given as a string rather than as a numeral value, the time is + * handled in a same way as it is handled when time is not given in a request. + */ + if (timeObject == null || timeObject.isTextual()) { + eventData.setTimeParsed(false); + eventData.setTimeSource("generated"); + if (previousEvent != null) { + if (previousEvent.isTimeParsed()) { + eventData.setTimeAsLong(previousEvent.getTimeAsLong()); + eventData.setTime(previousEvent.getTime()); + eventData.setTimeParsed(true); + eventData.setTimeSource("reported"); + } + } + /* + * If the time is given as epoch seconds with a decimal (example: + * 1433188255.253), the decimal point must be removed and time is assigned to + * HttpEventData object as a long value. convertTimeToEpochMillis() will check + * that correct time format is used. + */ + } else if (timeObject.isDouble()) { + eventData.setTimeAsLong(removeDecimal(timeObject.asDouble())); + eventData.setTime(String.valueOf(eventData.getTimeAsLong())); + eventData.setTimeParsed(true); + eventData.setTimeSource("reported"); + eventData = convertTimeToEpochMillis(eventData); + /* + * If the time is given in a numeral value, it is assigned to HttpEventData + * object as a long value. convertTimeToEpochMillis() will check that correct + * time format is used. + */ + } else if (timeObject.canConvertToLong()) { + eventData.setTimeAsLong(timeObject.asLong()); + eventData.setTime(jsonObject.get("time").asText()); + eventData.setTimeParsed(true); + eventData.setTimeSource("reported"); + eventData = convertTimeToEpochMillis(eventData); + } else { + eventData.setTimeParsed(false); + eventData.setTimeSource("generated"); + } + + return eventData; + } + + /* + * Takes a double value as a parameter, removes the decimal point from that + * value and returns the number as a long value. + */ + private long removeDecimal(double doubleValue) { + BigDecimal doubleValueWithDecimal = BigDecimal.valueOf(doubleValue); + String stringValue = doubleValueWithDecimal.toString(); + String stringValueWithoutDecimal = stringValue.replace(".", ""); + long longValue = Long.parseLong(stringValueWithoutDecimal); + + return longValue; + } + + /* + * Converts the given time stamp into epoch milliseconds. Takes a HttpEventData + * object as a parameter. Gets the time from the variable set in the + * HttpEventData object. If the time value in the object has 13 digits, it means + * that time has been already given in epoch milliseconds. + */ + private HttpEventData convertTimeToEpochMillis(HttpEventData eventData) { + String timeString = eventData.getTime(); + if (timeString.length() == 13) { + return eventData; + } else if (timeString.length() >= 10 && timeString.length() < 13) { + eventData.setTimeAsLong(eventData.getTimeAsLong() * (long) Math.pow(10, ((13 - timeString.length())))); + } else { + eventData.setTimeParsed(false); + eventData.setTimeSource("generated"); + } + return eventData; + } + + /* + * Assigns the metadata (authentication token and channel name) to the + * HttpEventData object. + */ + private HttpEventData assignMetaData(HttpEventData eventData, String authToken, String channel) { + + eventData.setAuthenticationToken(authToken); + eventData.setChannel(channel); + + return eventData; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/LifeCycle.java b/src/main/java/com/teragrep/cfe_16/LifeCycle.java new file mode 100644 index 0000000..dee9d4b --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/LifeCycle.java @@ -0,0 +1,62 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +/** + * An interface that own resources like threads for doing + * graceful shutdown. + * + */ +public interface LifeCycle { + + public void start(); + + /** + * This call is supposed to do a graceful cleanup of the implementing class. + */ + public void stop(); +} diff --git a/src/main/java/com/teragrep/cfe_16/RequestBodyCleaner.java b/src/main/java/com/teragrep/cfe_16/RequestBodyCleaner.java new file mode 100644 index 0000000..aadf2bc --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/RequestBodyCleaner.java @@ -0,0 +1,82 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.springframework.stereotype.Component; + +import java.util.regex.Pattern; + +/* + * Cleans the request body so, that only the body of the request is left in the string. + * This is needed when calling the endpoint that consumes MediaType.APPLICATION_FORM_URLENCODED_VALUE + * Example of body sent as a parameter: {channel=[CHANNEL_11111], {"sourcetype": "mysourcetype", "event": "Hello, world!"}=[]} + * Example of cleaned body returned by the cleanAckRequestBody(): {"sourcetype": "mysourcetype", "event": "Hello, world!"} + * TODO: Try to implement a better way to get the body of the request. + * + */ +@Component +public class RequestBodyCleaner { + + public String cleanAckRequestBody(String body, String channel) { + String bodyWithoutChannel = body.replaceAll("channel=\\[" + Pattern.quote(channel) + "\\]\\, ", ""); + String bodywithoutChannelLastCharRemoved = removeLastChar(bodyWithoutChannel); + + String cleanedBody = bodywithoutChannelLastCharRemoved.substring(1); + + cleanedBody = removeEqualsArrayFromEnd(cleanedBody); + + return cleanedBody; + } + + private String removeLastChar(String str) { + return str.substring(0, str.length() - 1); + } + + private String removeEqualsArrayFromEnd(String str) { + return str.replace("=[]", ""); + } +} diff --git a/src/main/java/com/teragrep/cfe_16/RequestHandler.java b/src/main/java/com/teragrep/cfe_16/RequestHandler.java new file mode 100644 index 0000000..8b3b337 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/RequestHandler.java @@ -0,0 +1,90 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.teragrep.cfe_16.bo.HeaderInfo; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * + */ +@Component +public class RequestHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(RequestHandler.class); + private String xForwardedFor; + private String xForwardedHost; + private String xForwardedProto; + + public HeaderInfo createHeaderInfoObject(HttpServletRequest request) { + LOGGER.debug("Creating new Header Info"); + HeaderInfo headerInfo = new HeaderInfo(); + xForwardedFor = request.getHeader("X-Forwarded-For"); + xForwardedHost = request.getHeader("X-Forwarded-Host"); + xForwardedProto = request.getHeader("X-Forwarded-Proto"); + if (xForwardedFor != null) { + LOGGER.debug("Setting X-Forwarded-For"); + LOGGER.trace("Setting X-Forwarded-For to value <[{}]>", xForwardedFor); + headerInfo.setxForwardedFor(xForwardedFor); + } + if (xForwardedHost != null) { + LOGGER.debug("Setting X-Forwarded-Host"); + LOGGER.trace("Setting X-Forwarded-Host to value <[{}]>", xForwardedHost); + headerInfo.setxForwardedHost(xForwardedHost); + } + if (xForwardedProto != null) { + LOGGER.debug("Setting X-Forwarded-Proto"); + LOGGER.trace("Setting X-Forwarded-Proto to value <[{}]>", xForwardedProto); + headerInfo.setxForwardedProto(xForwardedProto); + } + + return headerInfo; + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/SessionManager.java b/src/main/java/com/teragrep/cfe_16/SessionManager.java new file mode 100644 index 0000000..94dd2e5 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/SessionManager.java @@ -0,0 +1,177 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.teragrep.cfe_16.bo.Session; +import com.teragrep.cfe_16.config.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/* + * Manager that handles creating sessions and getting already existing sessions. + * Sessions are indexed by the authentication token. + * + */ +@Component +public class SessionManager implements Runnable, LifeCycle { + private static final Logger LOGGER = LoggerFactory.getLogger(SessionManager.class); + /** + * Maps auth token string => session object. + */ + private final Map sessions; + + /** + * Cleans up outdated Session objects. + */ + private Thread cleanerThread; + + @Autowired + private Configuration configuration; + + /** + * + */ + public SessionManager() { + this.sessions = new HashMap(); + } + + @Override + @PostConstruct + public void start() { + this.cleanerThread = new Thread(this, "Session cleaner"); + this.cleanerThread.start(); + } + + @Override + public void stop() { + this.cleanerThread.interrupt(); + } + + @Override + public void run() { + while (true) { + try { + LOGGER.debug("Sleeping for <{}> while waiting for poll", this.configuration.getPollTime()); + Thread.sleep(this.configuration.getPollTime()); + } catch (InterruptedException e) { + break; + } + synchronized (this) { + Iterator> iterator = this.sessions.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + long now = System.currentTimeMillis(); + long thresholdInLong = entry.getValue().getLastTouchedTimestamp() + this.configuration.getMaxSessionAge(); + if (now >= thresholdInLong) { + iterator.remove(); + } + } + } + } + } + + + + /* + * Gets a session for provided authentication token. returns null if + * there is no session for given authentication token + */ + public Session getSession(String authenticationToken) { + synchronized (this) { + Session session = this.sessions.get(authenticationToken); + return session; + } + } + + /** + * Returns an existing Session object based on authentication token. + * If no Session exists, a new one is created. + * + * @param authenticationToken + * @return + */ + public Session getOrCreateSession(String authenticationToken) { + LOGGER.debug("Getting or creating session"); + LOGGER.trace("Getting or creating session for authenticationToken: {}", authenticationToken); + synchronized (this) { + Session session = this.sessions.get(authenticationToken); + if (session == null) { + session = new Session(null, authenticationToken); + this.sessions.put(authenticationToken, session); + } + return session; + } + } + + public void removeSession(String authenticationToken) { + LOGGER.debug("Removing session"); + LOGGER.trace("Removing session for authenticationToken: {}", authenticationToken); + synchronized (this) { + this.sessions.remove(authenticationToken); + } + } + + /* + * Creates a new session object + */ + public Session createSession(String authenticationToken) { + LOGGER.debug("Creating new session"); + LOGGER.trace("Creating new session for authenticationToken: {}", authenticationToken); + synchronized (this) { + Session session = new Session(authenticationToken); + this.sessions.put(authenticationToken, session); + return session; + } + } +} diff --git a/src/main/java/com/teragrep/cfe_16/TestClient.java b/src/main/java/com/teragrep/cfe_16/TestClient.java new file mode 100644 index 0000000..912932f --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/TestClient.java @@ -0,0 +1,425 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.Socket; +import java.net.SocketException; + +/** + * A multithreaded load test client for the cfe_16 server. + * + */ +public class TestClient implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(TestClient.class); + /** + * How many loops a single thread does. + */ + private int n; + + /** + * Hostname or IP address of the cfe_16 server. + */ + private String host; + + /** + * TCP port of the cfe_16 server. + */ + private int port; + + public TestClient(int n, String host, int port) throws IOException { + this.n = n; + this.host = host; + this.port = port; + LOGGER.info("Initialized TestClient, sending <[{}]> messages to <[{}]>:<[{}]>", n, host, port); + } + + public void run() { + Socket socket; + try { + socket = createSocket(); + } catch (IOException e) { + throw new RuntimeException(e); + } + for (int i = 0; i < this.n; i++) { + + if (i % 10 == 0) { + System.out.print('.'); + } + + socket = testSendEvent(socket); + socket = testSendEventWithAckID(socket); + socket = testSendEventWithoutAuthorization(socket); + socket = testSendEventWithoutEventField(socket); + socket = testSendEventWithBlankEventField(socket); + socket = testGetAcksWithoutChannel(socket); + socket = testGetAcksWithInvalidChannel(socket); + } + try { + socket.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Socket testGetAcksWithInvalidChannel(Socket socket) { + String path = "/services/collector/ack?channel=CHANNEL_22222"; + String request = "{\"acks\": [1,3,4]}"; + String expectedRegex = "\\{\"text\":\"Invalid data channel\",\"code\":11,\"invalid-event-number\":0\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_BAD_REQUEST); + return socket; + } + + private Socket testGetAcksWithoutChannel(Socket socket) { + String path = "/services/collector/ack"; + String request = "{\"acks\": [1,3,4]}"; + String expectedRegex = "\\{\"text\":\"Data channel is missing\",\"code\":10,\"invalid-event-number\":0\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_BAD_REQUEST); + return socket; + } + + private Socket testSendEventWithBlankEventField(Socket socket) { + String path = "/services/collector?channel=00872DC6-AC83-4EDE-8AFE-8413C3825C4C"; + String request = "{\"sourcetype\": \"mysourcetype\", \"event\": \"\"}"; + String expectedRegex = "\\{\"text\":\"Event field cannot be blank\",\"code\":13,\"invalid-event-number\":0\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_BAD_REQUEST); + return socket; + } + + private Socket testSendEventWithoutEventField(Socket socket) { + String path = "/services/collector?channel=00872DC6-AC83-4EDE-8AFE-8413C3825C4C"; + String request = "{\"sourcetype\": \"mysourcetype\"}"; + String expectedRegex = "\\{\"text\":\"Event field is required\",\"code\":12,\"invalid-event-number\":0\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_BAD_REQUEST); + return socket; + } + + private Socket testSendEventWithoutAuthorization(Socket socket) { + String path = "/services/collector?channel=00872DC6-AC83-4EDE-8AFE-8413C3825C4C"; + String request = "{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\"}}"; + String expectedRegex = "\\{\"text\":\"Token is required\",\"code\":2,\"invalid-event-number\":0\\}"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, null, HttpURLConnection.HTTP_UNAUTHORIZED); + return socket; + } + + private Socket testSendEventWithAckID(Socket socket) { + String path = "/services/collector?channel=CHANNEL_11111"; + String request = "{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\"}"; + String expectedRegex = "\\{\"text\":\"Success\",\"code\":0,\"ackID\":([0-9])+\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_OK); + return socket; + } + + private Socket testSendEvent(Socket socket) { + String path = "/services/collector"; + String request = "{\"sourcetype\":\"access\", \"source\":\"/var/log/access.log\", \"event\": {\"message\":\"Access log test message\"}} {\"sourcetype\":\"access\", \"source\":\"/var/log/access.log\", \"event\": {\"message\":\"Access log test message 2\"}}"; + String expectedRegex = "\\{\"text\":\"Success\",\"code\":0\\}"; + String authorization = "AUTH_TOKEN_11111"; + socket = doRequestAndVerifyReply(socket, path, expectedRegex, request, authorization, HttpURLConnection.HTTP_OK); + return socket; + } + + private Socket createSocket() throws IOException { + Socket socket = new Socket(this.host, this.port); + socket.setKeepAlive(true); + socket.setTcpNoDelay(true); + return socket; + } + + private Socket doRequestAndVerifyReply(Socket socket, + String path, + String expectedRegex, + String request, + String authorization, + int expectedHttpStatusCode) { + while (true) { + try { + // ensure that the TCP connection is alive + socket = ensureTcpConnection(socket); + + // send the http request + sendRequest(socket, path, request, authorization); + + BufferedReader bufferedReader = getReader(socket); + // read first line of the HTTP response + String line; + try { + line = bufferedReader.readLine(); + if (line == null) { + // retry + cleanup(socket, bufferedReader); + continue; + } + } catch (SocketException e) { + // server RST'ed connection, retry + cleanup(socket, bufferedReader); + continue; + } + + // parse the status code + checkHttpStatusCode(expectedHttpStatusCode, line); + + // skip headers, check for Connection: close + // if connection is closed, a new socket is created + // but the response can still be read from bufferedReader + socket = readHeaders(socket, bufferedReader); + + // read the response + String responseBody = readResponseBody(bufferedReader); + // check if the response matches to regex + validateResponseBody(expectedRegex, responseBody); + // we are done + break; + } catch (SocketException e) { + // try again + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return socket; + } + + /** + * Validates the response body line against the given + * regural expression. + * + * @param expectedRegex + * @param responseBody + */ + private void validateResponseBody(String expectedRegex, String responseBody) { + if (!responseBody.matches(expectedRegex)) { + throw new RuntimeException(expectedRegex + " did not match to " + responseBody); + } + } + + /** + * Verifies that the HTTP reply status code was what it was + * supposed to me. + * + * @param expectedHttpStatusCode + * @param line + */ + private void checkHttpStatusCode(int expectedHttpStatusCode, String line) { + String statusCodeText = line.substring(9, 12); + int responseCode = Integer.parseInt(statusCodeText); + if (responseCode != expectedHttpStatusCode) { + throw new RuntimeException("HTTP/" + responseCode + " which was not expected."); + } + } + + /** + * Advances buffered reader through HTTP headers. + * If "Connection: close" header is seen, the socket + * is closed and a new one is created. + * + * @param socket + * @param bufferedReader + * @return + * @throws IOException + */ + private Socket readHeaders(Socket socket, BufferedReader bufferedReader) throws IOException { + String line; + while (true) { + line = bufferedReader.readLine(); + if (line.equals("Connection: close\r\n")) { + cleanup(socket, bufferedReader); + socket = this.createSocket(); + break; + } + if (line.equals("")) { + break; + } + } + return socket; + } + + /** + * Reads the reply line from the servere. Chunked transfer + * is assumed. + * + * @param bufferedReader + * @return + * @throws IOException + */ + private String readResponseBody(BufferedReader bufferedReader) throws IOException { + // skip chunked tag before body + bufferedReader.readLine(); + // read the json response body + String responseBody = bufferedReader.readLine(); + bufferedReader.readLine(); + return responseBody; + } + + private BufferedReader getReader(Socket socket) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + return bufferedReader; + } + + /** + * Checks that the TCP connection is open, otherwis a new + * connection is created and returned. + * + * @param socket + * @return + * @throws IOException + */ + private Socket ensureTcpConnection(Socket socket) throws IOException { + if (socket.isInputShutdown() || socket.isClosed() || !socket.isConnected()) { + socket.close(); + socket = createSocket(); + } + return socket; + } + + /** + * Does a HTTP request and keeps connection open. + * + * @param socket + * @param path + * @param request + * @param authorization + * @throws IOException + */ + private void sendRequest(Socket socket, String path, String request, String authorization) + throws IOException { + OutputStream outputStream = socket.getOutputStream(); + PrintWriter printWriter = new PrintWriter(outputStream); + printWriter.write("POST " + path + " HTTP/1.1\r\n"); + printWriter.write("Connection: keep-alive\r\n"); + printWriter.write("Content-Type: application/json;charset=UTF-8\r\n"); + printWriter.write("Accept: application/json\r\n"); + if (authorization != null) { + printWriter.write("Authorization: " + authorization + "\r\n"); + } + printWriter.write("User-Agent: cfe_16 test client v1\r\n"); + printWriter.write("Host: localhost:" + this.port + "\r\n"); + printWriter.write("Content-Length: " + request.length() + "\r\n"); + printWriter.write("\r\n"); + printWriter.write(request); + printWriter.flush(); + } + + private void cleanup(Socket socket, BufferedReader bufferedReader) throws IOException { + bufferedReader.close(); + socket.close(); + } + + public static void main(String[] args) throws IOException, InterruptedException { + if (args.length >= 4) { + String host = args[0]; + int port = Integer.valueOf(args[1]); + LOGGER.info("Connecting to <[{}]>:<[{}]>", host, port); + int numberOfThreads = Integer.valueOf(args[2]); + int numberOfLoops = Integer.valueOf(args[3]); + TestClient[] testClients = createTestClients(host, port, numberOfThreads, numberOfLoops); + Thread[] threads = createThreads(numberOfThreads, testClients); + long t1 = System.currentTimeMillis(); + startThreads(numberOfThreads, threads); + waitThreadsToFinish(numberOfThreads, threads); + long t2 = System.currentTimeMillis(); + long dt = t2 - t1; + int numberOfRequests = numberOfThreads * numberOfLoops; + double millisecsPerRequest = (double)dt / (double)numberOfRequests; + double throughput = 1000.0 / millisecsPerRequest; + LOGGER.info("Did <[{}]> requests in <{}> milliseconds, that is <{}> ms/req., which is <{}> transactions/sec.", numberOfRequests, dt, millisecsPerRequest, throughput); + FileOutputStream fileOutputStream = new FileOutputStream("stats.csv", true); + BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream)); + String line = numberOfThreads + "," + millisecsPerRequest + "," + throughput + "\n"; + bufferedWriter.write(line); + bufferedWriter.close(); + fileOutputStream.close(); + } else { + LOGGER.error("Usage: Usage: "); + } + } + + private static void waitThreadsToFinish(int numberOfThreads, Thread[] threads) throws InterruptedException { + for (int i = 0; i < numberOfThreads; i++) { + LOGGER.debug("Waiting for thread <{}> out of <{}> threads to finish", i, numberOfThreads); + threads[i].join(); + LOGGER.debug("Thread <[{}]> of <[{}]> threads finished", i, numberOfThreads); + } + } + + private static void startThreads(int numberOfThreads, Thread[] threads) { + LOGGER.debug("Starting <[{}]> threads", numberOfThreads); + for (int i = 0; i < numberOfThreads; i++) { + LOGGER.debug("Starting thread <{}> of <{}>", i, numberOfThreads); + threads[i].start(); + } + } + + private static Thread[] createThreads(int numberOfThreads, TestClient[] testClients) { + LOGGER.debug("Creating <[{}]> threads", numberOfThreads); + Thread[] threads = new Thread[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) { + LOGGER.debug("Creating thread <{}> of <[{}]>", i, numberOfThreads); + threads[i] = new Thread(testClients[i]); + } + return threads; + } + + private static TestClient[] createTestClients(String host, int port, int numberOfThreads, int numberOfLoops) + throws IOException { + LOGGER.debug("Creating <[{}]> test clients", numberOfThreads); + TestClient[] testClients = new TestClient[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) { + LOGGER.debug("Creating testClient <{}> of <[{}]>", i, numberOfThreads); + testClients[i] = new TestClient(numberOfLoops, host, port); + } + return testClients; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/ThirdParty/SyslogMessageSender/SyslogMessageSender.java b/src/main/java/com/teragrep/cfe_16/ThirdParty/SyslogMessageSender/SyslogMessageSender.java new file mode 100644 index 0000000..44e91a3 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/ThirdParty/SyslogMessageSender/SyslogMessageSender.java @@ -0,0 +1,289 @@ +/* + * Copyright 2010-2014, CloudBees Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.teragrep.cfe_16.ThirdParty.SyslogMessageSender; + +import com.cloudbees.syslog.SyslogMessage; +import com.cloudbees.syslog.sender.AbstractSyslogMessageSender; +import com.cloudbees.syslog.util.CachingReference; +import com.cloudbees.syslog.util.IoUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.*; +import java.math.BigInteger; +import java.net.*; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +/** + * See RFC 6587 - Transmission of Syslog Messages over TCP + * + * @author Cyrille Le Clerc + */ +@ThreadSafe +public class SyslogMessageSender extends AbstractSyslogMessageSender implements Closeable { + public final static int SETTING_SOCKET_CONNECT_TIMEOUT_IN_MILLIS_DEFAULT_VALUE = 500; + public final static int SETTING_MAX_RETRY = 2; + + /** + * {@link java.net.InetAddress InetAddress} of the remote Syslog Server. + * + * The {@code InetAddress} is refreshed regularly to handle DNS changes (default {@link #DEFAULT_INET_ADDRESS_TTL_IN_MILLIS}) + * + * Default value: {@link #DEFAULT_SYSLOG_HOST} + */ + protected CachingReference syslogServerHostnameReference; + /** + * Listen port of the remote Syslog server. + * + * Default: {@link #DEFAULT_SYSLOG_PORT} + */ + protected int syslogServerPort = DEFAULT_SYSLOG_PORT; + + private Socket socket; + private Writer writer; + private int socketConnectTimeoutInMillis = SETTING_SOCKET_CONNECT_TIMEOUT_IN_MILLIS_DEFAULT_VALUE; + private boolean ssl; + private SSLContext sslContext; + /** + * Number of retries to send a message before throwing an exception. + */ + private int maxRetryCount = SETTING_MAX_RETRY; + /** + * Number of exceptions trying to send message. + */ + protected final AtomicInteger trySendErrorCounter = new AtomicInteger(); + + // use the CR LF non transparent framing as described in "3.4.2. Non-Transparent-Framing" + private String postfix = "\n"; + + @Override + public synchronized void sendMessage(@Nonnull SyslogMessage message) throws IOException { + sendCounter.incrementAndGet(); + long nanosBefore = System.nanoTime(); + + try { + Exception lastException = null; + for (int i = 0; i <= maxRetryCount; i++) { + try { + if (logger.isLoggable(Level.FINEST)) { + logger.finest("Send syslog message " + message.toSyslogMessage(messageFormat)); + } + ensureSyslogServerConnection(); + message.toSyslogMessage(messageFormat, writer); + writer.write(postfix); + writer.flush(); + return; + } catch (IOException e) { + lastException = e; + IoUtils.closeQuietly(socket, writer); + trySendErrorCounter.incrementAndGet(); + } catch (RuntimeException e) { + lastException = e; + IoUtils.closeQuietly(socket, writer); + trySendErrorCounter.incrementAndGet(); + } + } + if (lastException != null) { + sendErrorCounter.incrementAndGet(); + if (lastException instanceof IOException) { + throw (IOException) lastException; + } else if (lastException instanceof RuntimeException) { + throw (RuntimeException) lastException; + } + } + } finally { + sendDurationInNanosCounter.addAndGet(System.nanoTime() - nanosBefore); + } + } + + private synchronized void ensureSyslogServerConnection() throws IOException { + InetAddress inetAddress = syslogServerHostnameReference.get(); + if (socket != null && !Objects.equals(socket.getInetAddress(), inetAddress)) { + logger.info("InetAddress of the Syslog Server have changed, create a new connection. " + + "Before=" + socket.getInetAddress() + ", new=" + inetAddress); + IoUtils.closeQuietly(socket, writer); + writer = null; + socket = null; + } + boolean socketIsValid; + try { + socketIsValid = socket != null && + socket.isConnected() + && socket.isBound() + && !socket.isClosed() + && !socket.isInputShutdown() + && !socket.isOutputShutdown(); + } catch (Exception e) { + socketIsValid = false; + } + if (!socketIsValid) { + writer = null; + try { + if (ssl) { + if (sslContext == null) { + socket = SSLSocketFactory.getDefault().createSocket(); + } else { + socket = sslContext.getSocketFactory().createSocket(); + } + } else { + socket = SocketFactory.getDefault().createSocket(); + } + socket.setKeepAlive(true); + socket.connect( + new InetSocketAddress(inetAddress, syslogServerPort), + socketConnectTimeoutInMillis); + + if (socket instanceof SSLSocket && logger.isLoggable(Level.FINER)) { + try { + SSLSocket sslSocket = (SSLSocket) socket; + SSLSession session = sslSocket.getSession(); + logger.finer("The Certificates used by peer"); + for (Certificate certificate : session.getPeerCertificates()) { + if (certificate instanceof X509Certificate) { + X509Certificate x509Certificate = (X509Certificate) certificate; + logger.finer("" + x509Certificate.getSubjectDN()); + } else { + logger.finer("" + certificate); + } + } + logger.finer("Peer host is " + session.getPeerHost()); + logger.finer("Cipher is " + session.getCipherSuite()); + logger.finer("Protocol is " + session.getProtocol()); + logger.finer("ID is " + new BigInteger(session.getId())); + logger.finer("Session created in " + session.getCreationTime()); + logger.finer("Session accessed in " + session.getLastAccessedTime()); + } catch (Exception e) { + logger.warn("Exception dumping debug info for " + socket, e); + } + } + } catch (IOException e) { + ConnectException ce = new ConnectException("Exception connecting to " + inetAddress + ":" + syslogServerPort); + ce.initCause(e); + throw ce; + } + } + if (writer == null) { + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), UTF_8)); + } + } + + @Override + public void setSyslogServerHostname(final String syslogServerHostname) { + this.syslogServerHostnameReference = new CachingReference(DEFAULT_INET_ADDRESS_TTL_IN_NANOS) { + @Nullable + @Override + protected InetAddress newObject() { + try { + return InetAddress.getByName(syslogServerHostname); + } catch (UnknownHostException e) { + throw new IllegalStateException(e); + } + } + }; + } + + @Override + public void setSyslogServerPort(int syslogServerPort) { + this.syslogServerPort = syslogServerPort; + } + + @Nullable + public String getSyslogServerHostname() { + if (syslogServerHostnameReference == null) + return null; + InetAddress inetAddress = syslogServerHostnameReference.get(); + return inetAddress == null ? null : inetAddress.getHostName(); + } + + public int getSyslogServerPort() { + return syslogServerPort; + } + + public boolean isSsl() { + return ssl; + } + + public void setSsl(boolean ssl) { + this.ssl = ssl; + } + + public synchronized void setSSLContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + + public synchronized SSLContext getSSLContext() { + return this.sslContext; + } + + public int getSocketConnectTimeoutInMillis() { + return socketConnectTimeoutInMillis; + } + + public int getMaxRetryCount() { + return maxRetryCount; + } + + public int getTrySendErrorCounter() { + return trySendErrorCounter.get(); + } + + public void setSocketConnectTimeoutInMillis(int socketConnectTimeoutInMillis) { + this.socketConnectTimeoutInMillis = socketConnectTimeoutInMillis; + } + + public void setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + } + + public synchronized void setPostfix(String postfix) { + this.postfix = postfix; + } + + @Override + public String toString() { + return getClass().getName() + "{" + + "syslogServerHostname='" + this.getSyslogServerHostname() + '\'' + + ", syslogServerPort='" + this.getSyslogServerPort() + '\'' + + ", ssl=" + ssl + + ", maxRetryCount=" + maxRetryCount + + ", socketConnectTimeoutInMillis=" + socketConnectTimeoutInMillis + + ", defaultAppName='" + defaultAppName + '\'' + + ", defaultFacility=" + defaultFacility + + ", defaultMessageHostname='" + defaultMessageHostname + '\'' + + ", defaultSeverity=" + defaultSeverity + + ", messageFormat=" + messageFormat + + ", sendCounter=" + sendCounter + + ", sendDurationInNanosCounter=" + sendDurationInNanosCounter + + ", sendErrorCounter=" + sendErrorCounter + + ", trySendErrorCounter=" + trySendErrorCounter + + '}'; + } + + @Override + public void close() throws IOException { + this.socket.close(); + } +} diff --git a/src/main/java/com/teragrep/cfe_16/TokenManager.java b/src/main/java/com/teragrep/cfe_16/TokenManager.java new file mode 100644 index 0000000..2af9bc3 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/TokenManager.java @@ -0,0 +1,119 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/* + * Manager that handles the authentication token + * + */ +@Component +public class TokenManager { + + public TokenManager() { + } + + /* + * Checks if the authentication token is in the request. Returns true if the + * authentication token is NOT in the request. Returns false if authentication + * token is found. + */ + // @LogAnnotation(type = LogType.DEBUG) + public boolean tokenIsMissing(HttpServletRequest request) { + + // AspectLoggerWrapper.log(new PayloadWrapper(OffsetDateTime.MAX, 4, + // LogValues.LogClass.audit, LogValues.GdprData.identification, "application", + // "environment", + // "component", "instance", LogValues.RetentionTime.P1Y, + // UUID.randomUUID().toString(), null)); + String authHeader = request.getHeader("Authorization"); + return (authHeader == null || authHeader.isEmpty()); + } + + /* + * Checks if the authentication token is given in basic authentication. Returns + * true if the token is in basic and false if not. Authorization header is given + * as a string parameter. + */ + // @LogAnnotation(type = LogType.DEBUG) + public boolean isTokenInBasic(String authHeader) { + + boolean isInBasic = false; + + if (authHeader != null && authHeader.toLowerCase().startsWith("basic")) { + isInBasic = true; + } + + return isInBasic; + } + + /* + * Trims the authorization header to get the authentication token from basic + * authentication form Authorization header is given as a string parameter. + * First the word "Basic" is trimmed out of the string. After that the + * credentials are decoded from Base64 and they are saved in a string that + * consists of username and a password. Here password is the authenication + * token. Username and password is separated into an array. Then we return the + * password (authentication token). + */ + // @LogAnnotation(type = LogType.DEBUG) + public String getTokenFromBasic(String authHeader) { + String base64Credentials = authHeader.substring("Basic".length()).trim(); + byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + + // credentials = username:password + final String[] values = credentials.split(":", 2); + + return values[1]; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/bo/Ack.java b/src/main/java/com/teragrep/cfe_16/bo/Ack.java new file mode 100644 index 0000000..e22c55d --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/bo/Ack.java @@ -0,0 +1,115 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.bo; + +/* + * Saves the status of the sent events. + * If the Ack is acknowledged, the event is sent succesfully. + * + */ +public class Ack { + + private int id; + private boolean acknowledged; + private long lastUsedTimestampInMilliseconds; + + public Ack() { + this(0, false); + } + + public Ack(int id, boolean acknowledged) { + this.id = id; + this.acknowledged = acknowledged; + this.lastUsedTimestampInMilliseconds = System.currentTimeMillis(); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public boolean isAcknowledged() { + return acknowledged; + } + + public void acknowledge() { + this.acknowledged = true; + } + + @Override + public String toString() { + return "Ack [id=" + id + ", acknowledged=" + acknowledged + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.id; + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + Ack ack = (Ack) o; + return id == ack.id; + } + + public void touch() { + this.lastUsedTimestampInMilliseconds = System.currentTimeMillis(); + } + + public long getLastUsedTimestamp() { + return this.lastUsedTimestampInMilliseconds; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/bo/HeaderInfo.java b/src/main/java/com/teragrep/cfe_16/bo/HeaderInfo.java new file mode 100644 index 0000000..3493572 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/bo/HeaderInfo.java @@ -0,0 +1,89 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.bo; + +/** + */ +public class HeaderInfo { + + private String xForwardedFor; + private String xForwardedHost; + private String xForwardedProto; + + public HeaderInfo() { + } + + public HeaderInfo(String xForwardedFor, String xForwardedHost, String xForwardedProto) { + this.xForwardedFor = xForwardedFor; + this.xForwardedHost = xForwardedHost; + this.xForwardedProto = xForwardedProto; + } + + public String getxForwardedFor() { + return xForwardedFor; + } + + public void setxForwardedFor(String xForwardedFor) { + this.xForwardedFor = xForwardedFor; + } + + public String getxForwardedHost() { + return xForwardedHost; + } + + public void setxForwardedHost(String xForwardedHost) { + this.xForwardedHost = xForwardedHost; + } + + public String getxForwardedProto() { + return xForwardedProto; + } + + public void setxForwardedProto(String xForwardedProto) { + this.xForwardedProto = xForwardedProto; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/bo/HttpEventData.java b/src/main/java/com/teragrep/cfe_16/bo/HttpEventData.java new file mode 100644 index 0000000..6eabf58 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/bo/HttpEventData.java @@ -0,0 +1,132 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.bo; + +/** + */ +public class HttpEventData { + + private String channel; + private String event; + private String authenticationToken; + private String timeSource; + private String time; + private long timeAsLong; + private boolean timeParsed; + private Integer ackID; + + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public String getAuthenticationToken() { + return authenticationToken; + } + + public void setAuthenticationToken(String authenticationToken) { + this.authenticationToken = authenticationToken; + } + + public String getTimeSource() { + return timeSource; + } + + public void setTimeSource(String timeSource) { + this.timeSource = timeSource; + } + + public String getTime() { + return time; + } + + public long getTimeAsLong() { + return timeAsLong; + } + + public void setTimeAsLong(long timeAsLong) { + this.timeAsLong = timeAsLong; + } + + public void setTime(String time) { + this.time = time; + } + + public boolean isTimeParsed() { + return timeParsed; + } + + public void setTimeParsed(boolean timeParsed) { + this.timeParsed = timeParsed; + } + + public Integer getAckID() { + return ackID; + } + + public void setAckID(Integer ackID) { + this.ackID = ackID; + } + + @Override + public String toString() { + return "HttpEventData [channel=" + channel + ", event=" + event + ", authenticationToken=" + authenticationToken + + ", timeSource=" + timeSource + ", time=" + time + ", timeAsLong=" + timeAsLong + ", timeParsed=" + + timeParsed + ", ackID=" + ackID + "]"; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/bo/Session.java b/src/main/java/com/teragrep/cfe_16/bo/Session.java new file mode 100644 index 0000000..8b2fdaa --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/bo/Session.java @@ -0,0 +1,147 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.bo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + + +/** + * A Session keeps track of channels that are contained + * inside one Session. This class is not thread-safe. + * + */ +public class Session { + + public static final String DEFAULT_CHANNEL = "defaultchannel"; + private static final Logger LOGGER = LoggerFactory.getLogger(Session.class); + /** + * Channels of this Session object. + */ + private Set channels; + + /** + * Authentication key of this Session. + */ + private String authenticationToken; + + private long lastTouchedTimestamp; + + @SuppressWarnings("unchecked") + public Session(String channel, String authenticationToken) { + LOGGER.info("Creating new session with channel <{}>", channel); + this.channels = Collections.synchronizedSet(new HashSet()); + if (channel != null) { + LOGGER.info("Adding channel <[{}]>", channel); + this.channels.add(channel); + } + this.authenticationToken = authenticationToken; + this.lastTouchedTimestamp = System.currentTimeMillis(); + } + + public Session(String authenticationToken) { + this(null, authenticationToken); + } + + public String getAuthenticationToken() { + return authenticationToken; + } + + public boolean addChannel(String channel) { + return this.channels.add(channel); + } + + public boolean doesChannelExist(String channel) { + return this.channels.contains(channel); + } + + public boolean removeChannel(String channel) { + return this.channels.remove(channel); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.authenticationToken == null) ? 0 : this.authenticationToken.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Session other = (Session) obj; + if (this.authenticationToken == null) { + if (other.authenticationToken != null) + return false; + } else if (!this.authenticationToken.equals(other.authenticationToken)) + return false; + return true; + } + + @Override + public String toString() { + return "channels=" + this.channels + ", authenticationToken=" + this.authenticationToken + "]"; + } + + public void touch() { + this.lastTouchedTimestamp = System.currentTimeMillis(); + } + + public long getLastTouchedTimestamp() { + return this.lastTouchedTimestamp; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/config/Configuration.java b/src/main/java/com/teragrep/cfe_16/config/Configuration.java new file mode 100644 index 0000000..075fb8e --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/config/Configuration.java @@ -0,0 +1,151 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * A Spring-utilizing class for getting configuration data. + * + */ + +@Component +public class Configuration { + + @Value("${syslog.server.host}") + private String sysLogHost; + + @Value("${syslog.server.protocol}") + private String sysLogProtocol; + + @Value("${syslog.server.port}") + private int sysLogPort; + + @Value("${max.ack.value}") + private int maxAckValue; + + @Value("${max.ack.age}") + private int maxAckAge; + + @Value("${max.session.age}") + private int maxSessionAge; + + @Value("${max.channels}") + private int maxChannels; + + @Value("${max.ack.value}") + private long pollTime; + + @Value("${server.print.times}") + private boolean printTimes; + + public Configuration() { + + } + + public String getSyslogHost() { + return this.sysLogHost; + } + + public String getSysLogProtocol() { + return this.sysLogProtocol; + } + + public void setSysLogProtocol(String sysLogProtocol) { + this.sysLogProtocol = sysLogProtocol; + } + + public void setSyslogHost(String syslogHost) { + this.sysLogHost = syslogHost; + } + + public int getSyslogPort() { + return this.sysLogPort; + } + + public void setSyslogPort(int syslogPort) { + this.sysLogPort = syslogPort; + } + + public int getMaxAckValue() { + return this.maxAckValue; + } + + public void setMaxAckValue(int maxAckValue) { + this.maxAckValue = maxAckValue; + } + + public int getMaxAckAge() { + return this.maxAckAge; + } + + public int getMaxChannels() { + return this.maxChannels; + } + + public long getPollTime() { + return this.pollTime; + } + + public boolean getPrintTimes() { + return this.printTimes; + } + + public int getMaxSessionAge() { + return this.maxSessionAge; + } + + @Override + public String toString() { + return "Configuration [sysLogHost=" + this.sysLogHost + ", sysLogProtocol=" + this.sysLogProtocol + + ", sysLogPort=" + this.sysLogPort + ", maxAckValue=" + this.maxAckValue + ", maxAckAge=" + + this.maxAckAge + ", maxSessionAge=" + this.maxSessionAge + ", maxChannels=" + this.maxChannels + + ", pollTime=" + this.pollTime + ", printTimes=" + this.printTimes + "]"; + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/AuthenticationTokenMissingException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/AuthenticationTokenMissingException.java new file mode 100644 index 0000000..b3e70b4 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/AuthenticationTokenMissingException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class AuthenticationTokenMissingException extends RuntimeException { + + public AuthenticationTokenMissingException() { + + } + + public AuthenticationTokenMissingException(String message) { + super(message); + } + + public AuthenticationTokenMissingException(Throwable cause) { + super(cause); + } + + public AuthenticationTokenMissingException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationTokenMissingException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotFoundException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotFoundException.java new file mode 100644 index 0000000..d4d3206 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotFoundException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class ChannelNotFoundException extends RuntimeException { + + public ChannelNotFoundException() { + super(); + } + + public ChannelNotFoundException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ChannelNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ChannelNotFoundException(String message) { + super(message); + } + + public ChannelNotFoundException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotProvidedException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotProvidedException.java new file mode 100644 index 0000000..4601711 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ChannelNotProvidedException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class ChannelNotProvidedException extends RuntimeException { + + public ChannelNotProvidedException() { + super(); + } + + public ChannelNotProvidedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ChannelNotProvidedException(String message, Throwable cause) { + super(message, cause); + } + + public ChannelNotProvidedException(String message) { + super(message); + } + + public ChannelNotProvidedException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldBlankException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldBlankException.java new file mode 100644 index 0000000..ace281d --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldBlankException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class EventFieldBlankException extends RuntimeException { + + public EventFieldBlankException() { + super(); + } + + public EventFieldBlankException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public EventFieldBlankException(String message, Throwable cause) { + super(message, cause); + } + + public EventFieldBlankException(String message) { + super(message); + } + + public EventFieldBlankException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldMissingException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldMissingException.java new file mode 100644 index 0000000..c1ddf83 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/EventFieldMissingException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class EventFieldMissingException extends RuntimeException { + + public EventFieldMissingException() { + super(); + } + + public EventFieldMissingException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public EventFieldMissingException(String message, Throwable cause) { + super(message, cause); + } + + public EventFieldMissingException(String message) { + super(message); + } + + public EventFieldMissingException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECErrorResponse.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECErrorResponse.java new file mode 100644 index 0000000..f0512cd --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECErrorResponse.java @@ -0,0 +1,92 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class HECErrorResponse { + + private String text; + private int code; + @JsonProperty("invalid-event-number") + private int invalidEventNumber; + + public HECErrorResponse() { + + } + + public HECErrorResponse(String text, int code, int invalidEventNumber) { + this.code = code; + this.text = text; + this.invalidEventNumber = invalidEventNumber; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public int getInvalidEventNumber() { + return invalidEventNumber; + } + + public void setInvalidEventNumber(int invalidEventNumber) { + this.invalidEventNumber = invalidEventNumber; + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECExceptionHandler.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECExceptionHandler.java new file mode 100644 index 0000000..822651a --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/HECExceptionHandler.java @@ -0,0 +1,129 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class HECExceptionHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(HECExceptionHandler.class); + @ExceptionHandler + public ResponseEntity handleException(AuthenticationTokenMissingException exc) { + + HECErrorResponse error = new HECErrorResponse("Token is required", 2, 0); + + return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler + public ResponseEntity handleException(ChannelNotFoundException exc) { + + HECErrorResponse error = new HECErrorResponse("Invalid data channel", 11, 0); + + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler + public ResponseEntity handleException(SessionNotFoundException exc) { + + HECErrorResponse error = new HECErrorResponse("Invalid token", 4, 0); + + return new ResponseEntity<>(error, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler + public ResponseEntity handleException(ChannelNotProvidedException exc) { + + HECErrorResponse error = new HECErrorResponse("Data channel is missing", 10, 0); + + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler + public ResponseEntity handleException(EventFieldMissingException exc) { + + HECErrorResponse error = new HECErrorResponse("Event field is required", 12, 0); + + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler + public ResponseEntity handleException(EventFieldBlankException exc) { + + HECErrorResponse error = new HECErrorResponse("Event field cannot be blank", 13, 0); // TODO: when support for + // multiple events with one + // request is implemented, + // get the real invalid + // event number here. + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler + public ResponseEntity handleException(ServerIsBusyException exc) { + + HECErrorResponse error = new HECErrorResponse("Server is busy", 9, 0); // TODO: when support for multiple events + // with one request is implemented, + // get the real invalid event number + // here. + return new ResponseEntity<>(error, HttpStatus.SERVICE_UNAVAILABLE); + } + + @ExceptionHandler + public ResponseEntity handleException(InternalServerErrorException e) { + + LOGGER.warn("Internal server error: ", e); + HECErrorResponse error = new HECErrorResponse("Internal server error ", 8, 0); + + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + + } +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/InternalServerErrorException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/InternalServerErrorException.java new file mode 100644 index 0000000..3ad30ea --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/InternalServerErrorException.java @@ -0,0 +1,80 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +public class InternalServerErrorException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public InternalServerErrorException() { + super(); + // TODO Auto-generated constructor stub + } + + public InternalServerErrorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + // TODO Auto-generated constructor stub + } + + public InternalServerErrorException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + + public InternalServerErrorException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + public InternalServerErrorException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/ServerIsBusyException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ServerIsBusyException.java new file mode 100644 index 0000000..c491a99 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/ServerIsBusyException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class ServerIsBusyException extends RuntimeException { + + public ServerIsBusyException() { + super(); + } + + public ServerIsBusyException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ServerIsBusyException(String message, Throwable cause) { + super(message, cause); + } + + public ServerIsBusyException(String message) { + super(message); + } + + public ServerIsBusyException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/exceptionhandling/SessionNotFoundException.java b/src/main/java/com/teragrep/cfe_16/exceptionhandling/SessionNotFoundException.java new file mode 100644 index 0000000..a91e50e --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/exceptionhandling/SessionNotFoundException.java @@ -0,0 +1,73 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.exceptionhandling; + +@SuppressWarnings("serial") +public class SessionNotFoundException extends RuntimeException { + + public SessionNotFoundException() { + super(); + } + + public SessionNotFoundException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public SessionNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public SessionNotFoundException(String message) { + super(message); + } + + public SessionNotFoundException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/teragrep/cfe_16/rest/HECRestController.java b/src/main/java/com/teragrep/cfe_16/rest/HECRestController.java new file mode 100644 index 0000000..70fb7b9 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/rest/HECRestController.java @@ -0,0 +1,250 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.rest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.teragrep.cfe_16.RequestBodyCleaner; +import com.teragrep.cfe_16.config.Configuration; +import com.teragrep.cfe_16.service.HECService; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/") +public class HECRestController { + private static final Logger LOGGER = LoggerFactory.getLogger(HECRestController.class); + @Autowired + private HECService service; + + @Autowired + private RequestBodyCleaner requestBodyCleaner; + + @Autowired + private Configuration configuration; + + private ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressWarnings("rawtypes") + @RequestMapping(value = "services/collector", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public JsonNode sendEvents(HttpServletRequest request, @RequestBody MultiValueMap body, + @RequestParam(required = false) String channel) { + // TODO: Try to think an alternative way to implement getting the body of the + // call + String eventInJson = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel); + + long t1 = System.nanoTime(); + JsonNode response = service.sendEvents(request, channel, eventInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + @RequestMapping(value = "services/collector", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public JsonNode sendEvents(HttpServletRequest request, @RequestBody String eventInJson, + @RequestParam(required = false) String channel) { + long t1 = System.nanoTime(); + JsonNode response = service.sendEvents(request, channel, eventInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @RequestMapping(value = "services/collector/ack", method = { RequestMethod.POST, + RequestMethod.GET }, consumes = MediaType.APPLICATION_JSON_VALUE) + public JsonNode getAcksWithPostMethod(@RequestBody JsonNode requestedAcksInJson, HttpServletRequest request, + @RequestParam(required = false) String channel) { + + long t1 = System.nanoTime(); + JsonNode response = service.getAcks(request, channel, requestedAcksInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("getAcks took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @SuppressWarnings("rawtypes") + @RequestMapping(value = "services/collector/ack", method = { RequestMethod.POST, + RequestMethod.GET }, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public @ResponseBody JsonNode getAcks(@RequestBody MultiValueMap body, HttpServletRequest request, + @RequestParam(required = false) String channel) { + // TODO: Try to think an alternative way to implement getting the body of the + // call + String bodyString = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel); + + JsonNode requestedAcksInJson = null; + try { + requestedAcksInJson = objectMapper.readValue(bodyString, JsonNode.class); + } catch (Exception e) { + // TODO: handle the error in a proper way + LOGGER.warn("Failed to handle response: ", e); + } + + long t1 = System.nanoTime(); + JsonNode response = service.getAcks(request, channel, requestedAcksInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("getAcks took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + @SuppressWarnings("rawtypes") + @RequestMapping(value = "services/collector/event", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public JsonNode sendEventsWithFormatOption(HttpServletRequest request, @RequestBody MultiValueMap body, + @RequestParam(required = false) String channel) { + + + // TODO: Try to think an alternative way to implement getting the body of the + // call + String eventInJson = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel); + + long t1 = System.nanoTime(); + JsonNode response = service.sendEvents(request, channel, eventInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + @RequestMapping(value = "services/collector/event", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public JsonNode sendEventsWithFormatOption(HttpServletRequest request, @RequestBody String eventInJson, + @RequestParam(required = false) String channel) { + // FIXME: Fix implementation to known standards + // This endpoint works identically to services/collector but introduces a format + // option for future scalability. + long t1 = System.nanoTime(); + JsonNode response = service.sendEvents(request, channel, eventInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @PostMapping("services/collector/event/1.0") + public JsonNode sendEventsWithProtocolVersion(HttpServletRequest request, @RequestBody String eventInJson, + @RequestParam(required = false) String channel) { + // FIXME: Fix implementation to known standards + // This endpoint works identically to services/collector/event but introduces a + // protocol version for future scalability + long t1 = System.nanoTime(); + JsonNode response = service.sendEvents(request, channel, eventInJson); + long t2 = System.nanoTime(); + long dt = t2 - t1; + double us = (double)dt / 1000.0; + if (this.configuration.getPrintTimes()) { + LOGGER.info("sendEvents took <{}¦ nanoseconds, that is <{}> microseconds", dt, us); + } + return response; + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @GetMapping("services/collector/health") + public ResponseEntity getHealth(HttpServletRequest request) { + // FIXME: Fix implementation to known standards + return service.healthCheck(request); + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @GetMapping("services/collector/health/1.0") + public String getHealthWithProtocolVersion() { + // TODO: Implement endpoint + return null; + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @PostMapping("services/collector/mint") + public void sendMintData(@RequestBody String mintData) { + // TODO: Implement endpoint + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @PostMapping("services/collector/mint/1.0") + public void sendMintDataWithProtocolVersion(@RequestBody String mintData) { + // TODO: Implement endpoint + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @PostMapping("services/collector/raw") + public void sendRawData(@RequestBody String rawData) { + // TODO: Implement endpoint + } + + // @LogAnnotation(type = LogType.METRIC_DURATION) + @PostMapping("services/collector/raw/1.0") + public void sendRawDataWithProtocolVersion(@RequestBody String rawData) { + // TODO: Implement endpoint + } +} diff --git a/src/main/java/com/teragrep/cfe_16/sender/AbstractSender.java b/src/main/java/com/teragrep/cfe_16/sender/AbstractSender.java new file mode 100644 index 0000000..dd1fef5 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/sender/AbstractSender.java @@ -0,0 +1,85 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.sender; + +import com.cloudbees.syslog.SyslogMessage; +import com.cloudbees.syslog.sender.AbstractSyslogMessageSender; + +import java.io.IOException; + +/** + * An abstract sender class for sending batch messages. + * + */ +public abstract class AbstractSender extends AbstractSyslogMessageSender { + + protected String hostname; + protected int port; + + protected AbstractSender(String hostname, int port) { + super(); + this.hostname = hostname; + this.port = port; + } + + /** + * Sends a batch of syslog messages. + * + * @param syslogMessages + */ + public abstract void sendMessages(SyslogMessage[] syslogMessages) throws IOException; + + @Override + public void setSyslogServerHostname(String syslogServerHostname) { + this.hostname = syslogServerHostname; + } + + @Override + public void setSyslogServerPort(int syslogServerPort) { + this.port = syslogServerPort; + } +} diff --git a/src/main/java/com/teragrep/cfe_16/sender/RelpSender.java b/src/main/java/com/teragrep/cfe_16/sender/RelpSender.java new file mode 100644 index 0000000..7b0d613 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/sender/RelpSender.java @@ -0,0 +1,162 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.sender; + +import com.cloudbees.syslog.SyslogMessage; +import com.teragrep.cfe_16.rest.HECRestController; +import com.teragrep.rlp_01.RelpBatch; +import com.teragrep.rlp_01.RelpConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeoutException; + +public class RelpSender extends AbstractSender { + + private final RelpConnection sender; + private static final Logger LOGGER = LoggerFactory.getLogger(RelpSender.class); + //settings for timeouts, if they are 0 that we skip them + //default are 0 + private int connectionTimeout = 10000; + private int readTimeout = 15000; + private int writeTimeout = 5000; + private int reconnectInterval = 500; + + public RelpSender(String hostname, int port) { + super(hostname, port); + this.sender = new RelpConnection(); + this.sender.setConnectionTimeout(connectionTimeout); + this.sender.setReadTimeout(this.readTimeout); + this.sender.setWriteTimeout(this.writeTimeout); + connect(); + } + + synchronized private void connect() { + boolean notConnected = true; + while (notConnected) { + boolean connected = false; + try { + LOGGER.debug("Connecting to RELP server"); + connected = this.sender.connect(this.hostname, this.port); + } catch (Exception e) { + LOGGER.warn("Failed to connect to RELP Server: ", e); + } + if (connected) { + notConnected = false; + } else { + try { + LOGGER.debug("Sleeping for <[{}]> before reconnecting", this.reconnectInterval); + Thread.sleep(this.reconnectInterval); + } catch (InterruptedException e) { + LOGGER.warn("Sleep interrupted: ", e); + } + } + } + } + + synchronized private void tearDown() { + LOGGER.debug("Tearing down connection"); + this.sender.tearDown(); + } + + synchronized private void disconnect() { + try { + LOGGER.debug("Disconnecting from RELP server"); + this.sender.disconnect(); + } catch (IllegalStateException | IOException | TimeoutException e) { + LOGGER.warn("Failed to disconnect from RELP Server: ", e); + } + finally { + this.tearDown(); + } + } + + @Override + synchronized public void close() { + this.disconnect(); + } + + @Override + synchronized public void sendMessages(SyslogMessage[] syslogMessages) throws IOException { + final RelpBatch relpBatch = new RelpBatch(); + for (SyslogMessage syslogMessage : syslogMessages) { + relpBatch.insert(syslogMessage.toRfc5424SyslogMessage().getBytes("UTF-8")); + } + doSend(relpBatch); + } + + @Override + synchronized public void sendMessage(SyslogMessage syslogMessage) throws IOException { + final RelpBatch relpBatch = new RelpBatch(); + relpBatch.insert(syslogMessage.toRfc5424SyslogMessage().getBytes("UTF-8")); + doSend(relpBatch); + } + + synchronized private void doSend(RelpBatch relpBatch) { + boolean notSent = true; + + while (notSent) { + try { + LOGGER.debug("Committing a RELP batch"); + this.sender.commit(relpBatch); + } catch (IllegalStateException | IOException | TimeoutException e) { + LOGGER.warn("Failed to commit batch: ", e); + } + + if (!relpBatch.verifyTransactionAll()) { + LOGGER.debug("Failed to verify all transactions, retrying them"); + relpBatch.retryAllFailed(); + this.tearDown(); + this.connect(); + } else { + notSent = false; + } + } + } +} diff --git a/src/main/java/com/teragrep/cfe_16/sender/SenderFactory.java b/src/main/java/com/teragrep/cfe_16/sender/SenderFactory.java new file mode 100644 index 0000000..781a35c --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/sender/SenderFactory.java @@ -0,0 +1,68 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.sender; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class SenderFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(SenderFactory.class); + public static AbstractSender createSender(String type, String hostname, int port) throws IOException { + LOGGER.debug("Creating sender for type <[{}]> to <[{}]>:<[{}]>", type, hostname, port); + if (type.equalsIgnoreCase("UDP")) { + return new UdpSender(hostname, port); + } else if (type.equalsIgnoreCase("TCP")) { + return new TcpSender(hostname, port); + } else if (type.equalsIgnoreCase("RELP")) { + return new RelpSender(hostname, port); + } else { + throw new IOException("Invalid sender type: " + type); + } + } +} diff --git a/src/main/java/com/teragrep/cfe_16/sender/TcpSender.java b/src/main/java/com/teragrep/cfe_16/sender/TcpSender.java new file mode 100644 index 0000000..11f05b5 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/sender/TcpSender.java @@ -0,0 +1,91 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.sender; + +import com.cloudbees.syslog.SyslogMessage; +import com.cloudbees.syslog.sender.TcpSyslogMessageSender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class TcpSender extends AbstractSender { + private static final Logger LOGGER = LoggerFactory.getLogger(TcpSender.class); + private TcpSyslogMessageSender sender; + + public TcpSender(String hostname, int port) { + super(hostname, port); + this.sender = new TcpSyslogMessageSender(); + this.sender.setSyslogServerHostname(this.hostname); + this.sender.setSyslogServerPort(this.port); + } + + @Override + public void sendMessages(SyslogMessage[] syslogMessages) throws IOException { + LOGGER.debug("Sending messages"); + for (SyslogMessage syslogMessage : syslogMessages) { + this.sender.sendMessage(syslogMessage); + } + } + + @Override + public void sendMessage(SyslogMessage syslogMessage) throws IOException { + LOGGER.debug("Sending message"); + this.sender.sendMessage(syslogMessage); + } + + @Override + public void close() throws IOException { + LOGGER.debug("Closing sender"); + this.sender.close(); + } + + public void setSsl(boolean ssl) { + LOGGER.debug("Set Ssl to <{}>", ssl); + this.sender.setSsl(ssl); + } +} diff --git a/src/main/java/com/teragrep/cfe_16/sender/UdpSender.java b/src/main/java/com/teragrep/cfe_16/sender/UdpSender.java new file mode 100644 index 0000000..7c7d310 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/sender/UdpSender.java @@ -0,0 +1,86 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.sender; + +import com.cloudbees.syslog.SyslogMessage; +import com.cloudbees.syslog.sender.UdpSyslogMessageSender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class UdpSender extends AbstractSender { + private static final Logger LOGGER = LoggerFactory.getLogger(UdpSender.class); + private UdpSyslogMessageSender sender; + + public UdpSender(String hostname, int port) { + super(hostname, port); + this.sender = new UdpSyslogMessageSender(); + this.sender.setSyslogServerHostname(this.hostname); + this.sender.setSyslogServerPort(this.port); + } + + @Override + public void sendMessages(SyslogMessage[] syslogMessages) throws IOException { + LOGGER.debug("Sending messages"); + for (SyslogMessage syslogMessage : syslogMessages) { + this.sender.sendMessage(syslogMessage); + } + } + + @Override + public void sendMessage(SyslogMessage syslogMessage) throws IOException { + LOGGER.debug("Sending message"); + this.sender.sendMessage(syslogMessage); + } + + @Override + public void close() throws IOException { + LOGGER.debug("Closing sender"); + this.sender.close(); + } +} diff --git a/src/main/java/com/teragrep/cfe_16/service/HECService.java b/src/main/java/com/teragrep/cfe_16/service/HECService.java new file mode 100644 index 0000000..a6625bf --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/service/HECService.java @@ -0,0 +1,87 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.service; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; + +/** + * An interface that specified the REST back end API. + * + */ +public interface HECService { + + /** + * Returns the JSON object as a response of given HTTP event request. + * + * @param request + * @param channel + * @param eventInJson + * @return + */ + public ObjectNode sendEvents(HttpServletRequest request, String channel, String eventInJson); + + /** + * + * @param request + * @param channel + * @param requestedAcksInJson + * @return + */ + public JsonNode getAcks(HttpServletRequest request, String channel, JsonNode requestedAcksInJson); + + /** + * Ping. + * + * @param request + * @return + */ + public ResponseEntity healthCheck(HttpServletRequest request); +} diff --git a/src/main/java/com/teragrep/cfe_16/service/HECServiceImpl.java b/src/main/java/com/teragrep/cfe_16/service/HECServiceImpl.java new file mode 100644 index 0000000..aba5012 --- /dev/null +++ b/src/main/java/com/teragrep/cfe_16/service/HECServiceImpl.java @@ -0,0 +1,197 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.teragrep.cfe_16.*; +import com.teragrep.cfe_16.bo.HeaderInfo; +import com.teragrep.cfe_16.bo.Session; +import com.teragrep.cfe_16.exceptionhandling.AuthenticationTokenMissingException; +import com.teragrep.cfe_16.exceptionhandling.ChannelNotFoundException; +import com.teragrep.cfe_16.exceptionhandling.ChannelNotProvidedException; +import com.teragrep.cfe_16.exceptionhandling.SessionNotFoundException; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +/** + * Implementation of the REST Service back end. + * + */ +@Service +public class HECServiceImpl implements HECService { + private static final Logger LOGGER = LoggerFactory.getLogger(HECServiceImpl.class); + @Autowired + private AckManager ackManager; + + @Autowired + private SessionManager sessionManager; + + @Autowired + private TokenManager tokenManager; + + @Autowired + private EventManager eventManager; + + @Autowired + private RequestHandler requestHandler; + + private ObjectMapper objectMapper = new ObjectMapper(); + + public HECServiceImpl() { + } + + @Override + // @LogAnnotation(type = LogType.METRIC_COUNTER) + public ObjectNode sendEvents(HttpServletRequest request, + String channel, + String eventInJson) { + LOGGER.debug("Sending events to channel <{}>", channel); + if (this.tokenManager.tokenIsMissing(request)) { + throw new AuthenticationTokenMissingException("Authentication token must be provided"); + } + // AspectLoggerWrapper.logMetricCounter(null, "metric_counter", 10); + // AspectLoggerWrapper.logMetricDuration(null, "new_metric", + // MetricDurationOptionsImpl.MetricDuration.P10S); + String authHeader = request.getHeader("Authorization"); + HeaderInfo headerInfo = requestHandler.createHeaderInfoObject(request); + + String authToken; + if (tokenManager.isTokenInBasic(authHeader)) { + LOGGER.debug("Token was provided via Basic"); + authToken = this.tokenManager.getTokenFromBasic(authHeader); + } else { + LOGGER.debug("Token was provided via header"); + authToken = authHeader; + } + + // if there is no channel, we'll use the default channel + if (channel == null) { + channel = Session.DEFAULT_CHANNEL; + LOGGER.debug("Channel was not provided, using <{}>", channel); + } + + Session session = this.sessionManager.getOrCreateSession(authToken); + + // if the channel is not in the session, let's add the channel into it + if (!session.doesChannelExist(channel)) { + LOGGER.debug("Adding channel <{}>", channel); + session.addChannel(channel); + } + + // TODO: find a nice way of not passing AckManager instance + ObjectNode ackNode = this.eventManager.convertData(authToken, + channel, + eventInJson, + headerInfo, + this.ackManager); + + return ackNode; + } + + // @LogAnnotation(type = LogType.RESPONSE) + @SuppressWarnings("deprecation") + @Override + public JsonNode getAcks(HttpServletRequest request, + String channel, + JsonNode requestedAcksInJson) { + + // filter out error cases + // authentication header is required always + if (this.tokenManager.tokenIsMissing(request)) { + throw new AuthenticationTokenMissingException("Authentication token must be provided"); + } + + String authHeader = request.getHeader("Authorization"); + + String authToken; + if (tokenManager.isTokenInBasic(authHeader)) { + LOGGER.debug("Token was provided via Basic"); + authToken = this.tokenManager.getTokenFromBasic(authHeader); + } else { + LOGGER.debug("Token was provided via header"); + authToken = authHeader; + } + + // channel is required + if (channel == null) { + throw new ChannelNotProvidedException("Channel must be provided when requesting ack statuses"); + } + + // session is also required + Session session = this.sessionManager.getSession(authToken); + if (session == null) { + throw new SessionNotFoundException("Session not found for auth token " + authToken); + } + + // if channel is not inside Session, it is considered an error case + if (!session.doesChannelExist(channel)) { + throw new ChannelNotFoundException(); + } + session.touch(); + + ObjectNode responseNode = objectMapper.createObjectNode(); + JsonNode requestedAckStatuses = this.ackManager.getRequestedAckStatuses(authToken, channel, requestedAcksInJson); + responseNode.put("acks", requestedAckStatuses); + return responseNode; + } + + // @LogAnnotation(type = LogType.RESPONSE) + @Override + public ResponseEntity healthCheck(HttpServletRequest request) { + if (this.tokenManager.tokenIsMissing(request)) { + return new ResponseEntity("Invalid HEC token", HttpStatus.BAD_REQUEST); + } + return new ResponseEntity("HEC is available and accepting input", HttpStatus.OK); + } +} diff --git a/src/main/resources/application.properties.example b/src/main/resources/application.properties.example new file mode 100644 index 0000000..8ba1f51 --- /dev/null +++ b/src/main/resources/application.properties.example @@ -0,0 +1,11 @@ +syslog.server.host=127.0.0.1 +syslog.server.port=1234 +syslog.server.protocol=TCP +max.channels=1000000 +max.ack.value=1000000 +max.ack.age=20000 +max.session.age=30000 +poll.time=300000 +config.poll.time=5000 +spring.devtools.add-properties=false +server.print.times=true diff --git a/src/main/resources/control.sh b/src/main/resources/control.sh new file mode 100755 index 0000000..4f2be82 --- /dev/null +++ b/src/main/resources/control.sh @@ -0,0 +1,159 @@ +#!/usr/bin/bash +# +# HTTP Event Capture to RFC5424 CFE_16 +# Copyright (C) 2021 Suomen Kanuuna Oy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# +# Additional permission under GNU Affero General Public License version 3 +# section 7 +# +# If you modify this Program, or any covered work, by linking or combining it +# with other code, such other code is not for that reason alone subject to any +# of the requirements of the GNU Affero GPL version 3 as long as this Program +# is the same Program as licensed from Suomen Kanuuna Oy without any additional +# modifications. +# +# Supplemented terms under GNU Affero General Public License version 3 +# section 7 +# +# Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified +# versions must be marked as "Modified version of" The Program. +# +# Names of the licensors and authors may not be used for publicity purposes. +# +# No rights are granted for use of trade names, trademarks, or service marks +# which are in The Program if any. +# +# Licensee must indemnify licensors and authors for any liability that these +# contractual assumptions impose on licensors and authors. +# +# To the extent this program is licensed as part of the Commercial versions of +# Teragrep, the applicable Commercial License may apply to this file if you as +# a licensee so wish it. +# + +ulimit -n 65535 + +RETVAL=0 +prog="Teragrep HTTP Event Capture" + +start() { + echo "Starting $prog: " + + if [ ! -r /opt/teragrep/cfe_16/etc/application.properties ]; then + echo "Configuration file /opt/teragrep/cfe_16/etc/application.properties does not exist" + exit 1 + fi + + if [ -r /opt/teragrep/cfe_16/var/cfe_16.pid ]; then + # check if running + cfe16_pid=$(cat /opt/teragrep/cfe_16/var/cfe_16.pid) + + (ps -f -p $cfe16_pid | grep cfe_16.jar > /dev/null 2>&1) + running=$? + + if [ "$running" = 0 ]; then + echo "Already running with ${cfe16_pid}" + RETVAL=1 + return 1 + fi + fi + + ## + GENERAL_OPTS="-d64" + HEAPSIZE_OPTS="-Xmx192M -Xms192M" + AGENT_OPTS="-javaagent:/opt/teragrep/cfe_16/share/aspectjweaver.jar" + + #JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=1100 -Djava.rmi.server.hostname=localhost" + + GC_OPTS="-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled" + GC_DEBUG_OPTS="" # -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTLAB + + /usr/bin/nohup /usr/bin/java $GENERAL_OPTS $HEAPSIZE_OPTS $AGENT_OPTS $JMX_OPTS $THREAD_OPTS $GC_OPTS $GC_DEBUG_OPTS -jar /opt/teragrep/cfe_16/share/cfe_16.jar --spring.config.location=file:///opt/teragrep/cfe_16/etc/application.properties > /opt/teragrep/cfe_16/var/stdout.log 2> /opt/teragrep/cfe_16/var/stderr.log & + RETVAL=$? + + # store pid + echo $! > /opt/teragrep/cfe_16/var/cfe_16.pid + + [ "$RETVAL" = 0 ] && echo "OK" + echo +} + +stop() { + RETVAL=1 + cfe16_pid="" + echo "Stopping $prog: " + + if [ -r /opt/teragrep/cfe_16/var/cfe_16.pid ]; then + # check if running + cfe16_pid=$(cat /opt/teragrep/cfe_16/var/cfe_16.pid) + + (ps -f -p $cfe16_pid | grep cfe_16.jar > /dev/null 2>&1) + running=$? + + if [ "$running" = 0 ]; then + kill $cfe16_pid + while `ps -p $cfe16_pid > /dev/null 2>&1`; do sleep 1; done + RETVAL=$? + fi + fi + + [ "$RETVAL" = 0 ] && echo "OK" + echo +} + +status() { + RETVAL=1 + echo "$prog status: " + + if [ -r /opt/teragrep/cfe_16/var/cfe_16.pid ]; then + # check if running + cfe16_pid=$(cat /opt/teragrep/cfe_16/var/cfe_16.pid) + + (ps -f -p $cfe16_pid | grep cfe_16.jar > /dev/null 2>&1) + running=$? + + if [ "$running" = 0 ]; then + echo "Pid: ${cfe16_pid}" + RETVAL=0 + return 0 + fi + fi + echo +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + reload) + reload + ;; + status) + status + ;; + *) + echo "Usage: $0 {start|stop|restart|reload|status}" + RETVAL=1 + ;; +esac diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..28bd669 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/teragrep/cfe_16/Cfe16ApplicationTests.java b/src/test/java/com/teragrep/cfe_16/Cfe16ApplicationTests.java new file mode 100644 index 0000000..4fa737c --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/Cfe16ApplicationTests.java @@ -0,0 +1,56 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.junit.jupiter.api.Test; + +public class Cfe16ApplicationTests { + + @Test + public void contextLoads() { + } +} diff --git a/src/test/java/com/teragrep/cfe_16/CleanRequestBodyTests.java b/src/test/java/com/teragrep/cfe_16/CleanRequestBodyTests.java new file mode 100644 index 0000000..a6f9e36 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/CleanRequestBodyTests.java @@ -0,0 +1,61 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CleanRequestBodyTests { + @Test + public void testCleanRequestBodyNormal() { + String input = "{channel=[CHANNEL_11111], {\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\"}=[]}"; + String channel = "CHANNEL_11111"; + RequestBodyCleaner requestBodyCleaner = new RequestBodyCleaner(); + String cleaned = requestBodyCleaner.cleanAckRequestBody(input, channel); + Assertions.assertEquals("{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\"}", cleaned, "Did not clean channel properly"); + } +} diff --git a/src/test/java/com/teragrep/cfe_16/ConverterTests.java b/src/test/java/com/teragrep/cfe_16/ConverterTests.java new file mode 100644 index 0000000..9b1630b --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/ConverterTests.java @@ -0,0 +1,371 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.cloudbees.syslog.*; +import com.teragrep.cfe_16.bo.HeaderInfo; +import com.teragrep.cfe_16.bo.HttpEventData; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; + +/* + * Tests the functionality of Converter + */ +public class ConverterTests { + + private Converter converter; + private HttpEventData eventData1; + private HttpEventData eventData2; + private HttpEventData eventData3; + private Severity supposedSeverity; + private Facility supposedFacility; + private SyslogMessage supposedSyslogMessage1; + private SyslogMessage supposedSyslogMessage2; + private SyslogMessage supposedSyslogMessage3; + private SDElement metadataSDE1; + private SDElement metadataSDE2; + private SDElement metadataSDE3; + private SyslogMessage returnedMessage1; + private SyslogMessage returnedMessage2; + private SyslogMessage returnedMessage3; + private Set returnedSDElements1; + private Set returnedSDElements2; + private Set returnedSDElements3; + private Set supposedSDElements1; + private Set supposedSDElements2; + private Set supposedSDElements3; + + /* + * Initializes 3 HttpEventData objects and the data for them, 3 Structured Data + * Elements (SDE) with this data and 3 SyslogMessages with the help of those + * SDE's. We then call Converter's httpToSyslog() method to convert our + * HttpEventData objects so we have 3 supposed messages and 3 returned messages + * from Converter. SDElements from all of those messages are saved in Sets so + * that they can be compared to each other in the test methods. + */ + @BeforeEach + public void initialize() { + + converter = new Converter(); + + eventData1 = new HttpEventData(); + eventData2 = new HttpEventData(); + eventData3 = new HttpEventData(); + + supposedSyslogMessage1 = null; + supposedSyslogMessage2 = null; + supposedSyslogMessage3 = null; + metadataSDE1 = new SDElement("cfe_16-metadata@48577"); + metadataSDE2 = new SDElement("cfe_16-metadata@48577"); + metadataSDE3 = new SDElement("cfe_16-metadata@48577"); + + supposedSeverity = Severity.INFORMATIONAL; + supposedFacility = Facility.USER; + + eventData1.setAuthenticationToken("AUTH_TOKEN_11111"); + eventData1.setChannel("CHANNEL_11111"); + eventData1.setAckID(0); + eventData1.setTimeSource("reported"); + eventData1.setTime("1433188255253"); + eventData1.setTimeParsed(true); + eventData1.setTimeAsLong(1433188255253L); + eventData1.setEvent("Event 1"); + + eventData2.setAuthenticationToken("AUTH_TOKEN_22222"); + eventData2.setChannel("CHANNEL_22222"); + eventData2.setAckID(1); + eventData2.setTimeSource("generated"); + eventData2.setTimeParsed(false); + eventData2.setEvent("Event 2"); + + eventData3.setAuthenticationToken("AUTH_TOKEN_33333"); + eventData3.setChannel("defaultchannel"); + eventData3.setTimeSource("generated"); + eventData3.setTimeParsed(false); + eventData3.setEvent("Event 3"); + + metadataSDE1.addSDParam("authentication_token", eventData1.getAuthenticationToken()); + metadataSDE1.addSDParam("channel", eventData1.getChannel()); + metadataSDE1.addSDParam("ack_id", String.valueOf(eventData1.getAckID())); + metadataSDE1.addSDParam("time_source", eventData1.getTimeSource()); + metadataSDE1.addSDParam("time_parsed", "true"); + metadataSDE1.addSDParam("time", eventData1.getTime()); + + metadataSDE2.addSDParam("authentication_token", eventData2.getAuthenticationToken()); + metadataSDE2.addSDParam("channel", eventData2.getChannel()); + metadataSDE2.addSDParam("ack_id", String.valueOf(eventData2.getAckID())); + metadataSDE2.addSDParam("time_source", eventData2.getTimeSource()); + + metadataSDE3.addSDParam("authentication_token", eventData3.getAuthenticationToken()); + metadataSDE3.addSDParam("channel", eventData3.getChannel()); + metadataSDE3.addSDParam("time_source", eventData3.getTimeSource()); + + supposedSyslogMessage1 = new SyslogMessage().withTimestamp(eventData1.getTimeAsLong()) + .withSeverity(supposedSeverity).withAppName("capsulated").withHostname("cfe_16") + .withFacility(supposedFacility).withSDElement(metadataSDE1).withMsg(eventData1.getEvent()); + + supposedSyslogMessage2 = new SyslogMessage().withSeverity(supposedSeverity).withAppName("capsulated") + .withHostname("cfe_16").withFacility(supposedFacility).withSDElement(metadataSDE2) + .withMsg(eventData2.getEvent()); + + supposedSyslogMessage3 = new SyslogMessage().withSeverity(supposedSeverity).withAppName("capsulated") + .withHostname("cfe_16").withFacility(supposedFacility).withSDElement(metadataSDE3) + .withMsg(eventData3.getEvent()); + HeaderInfo headerInfo = new HeaderInfo(); + + returnedMessage1 = converter.httpToSyslog(eventData1, headerInfo); + returnedMessage2 = converter.httpToSyslog(eventData2, headerInfo); + returnedMessage3 = converter.httpToSyslog(eventData3, headerInfo); + + returnedSDElements1 = returnedMessage1.getSDElements(); + returnedSDElements2 = returnedMessage2.getSDElements(); + returnedSDElements3 = returnedMessage3.getSDElements(); + supposedSDElements1 = supposedSyslogMessage1.getSDElements(); + supposedSDElements2 = supposedSyslogMessage2.getSDElements(); + supposedSDElements3 = supposedSyslogMessage3.getSDElements(); + + } + + /* + * Compares the severity from the supposed SyslogMessage and the SyslogMessage + * returned from Converter. Severity should be hardcoded to INFORMATIONAL. + */ + @Test + public void severityTest() { + assertEquals("Severity should be INFORMATIONAL", supposedSyslogMessage1.getSeverity(), + returnedMessage1.getSeverity()); + assertEquals("Severity should be INFORMATIONAL", supposedSyslogMessage2.getSeverity(), + returnedMessage2.getSeverity()); + assertEquals("Severity should be INFORMATIONAL", supposedSyslogMessage3.getSeverity(), + returnedMessage3.getSeverity()); + } + + /* + * Compares the facility from the supposed SyslogMessage and the SyslogMessage + * returned from Converter. Facility should be hardcoded to USER. + */ + @Test + public void facilityTest() { + assertEquals("Facility should be USER", supposedSyslogMessage1.getFacility(), returnedMessage1.getFacility()); + assertEquals("Facility should be USER", supposedSyslogMessage2.getFacility(), returnedMessage2.getFacility()); + assertEquals("Facility should be USER", supposedSyslogMessage3.getFacility(), returnedMessage3.getFacility()); + } + + /* + * Compares the AppName and HostName from the supposed SyslogMessage and the + * SyslogMessage returned from Converter. AppName should be hardcoded to + * "capsulated" and HostName should be hardcoded to "cfe_16". + */ + @Test + public void appNameAndHostNameTest() { + assertEquals("App name should be '" + supposedSyslogMessage1.getAppName() + "'", + supposedSyslogMessage1.getAppName(), returnedMessage1.getAppName()); + assertEquals("App name should be '" + supposedSyslogMessage2.getAppName() + "'", + supposedSyslogMessage2.getAppName(), returnedMessage2.getAppName()); + assertEquals("App name should be '" + supposedSyslogMessage3.getAppName() + "'", + supposedSyslogMessage3.getAppName(), returnedMessage3.getAppName()); + + assertEquals("Host name should be '" + supposedSyslogMessage1.getHostname() + "'", + supposedSyslogMessage1.getHostname(), returnedMessage1.getHostname()); + assertEquals("Host name should be '" + supposedSyslogMessage1.getHostname() + "'", + supposedSyslogMessage2.getHostname(), returnedMessage2.getHostname()); + assertEquals("Host name should be '" + supposedSyslogMessage1.getHostname() + "'", + supposedSyslogMessage3.getHostname(), returnedMessage3.getHostname()); + } + + /* + * Compares the msg from the supposed SyslogMessage and the SyslogMessage + * returned from Converter. msg should be the event from HttpEventData object. + */ + @Test + public void msgTest() { + assertEquals("Msg should be '" + supposedSyslogMessage1.getMsg().toString() + "'", + supposedSyslogMessage1.getMsg().toString(), returnedMessage1.getMsg().toString()); + assertEquals("Msg should be '" + supposedSyslogMessage2.getMsg().toString() + "'", + supposedSyslogMessage2.getMsg().toString(), returnedMessage2.getMsg().toString()); + assertEquals("Msg should be '" + supposedSyslogMessage3.getMsg().toString() + "'", + supposedSyslogMessage3.getMsg().toString(), returnedMessage3.getMsg().toString()); + } + + /* + * Compares the time stamp from the supposed SyslogMessage and the SyslogMessage + * returned from Converter if the time stamp is assigned to the SyslogMessage. + * Otherwise time stamp should be null. + */ + @Test + public void timestampTest() { + + assertEquals("Timestamp should be: " + supposedSyslogMessage1.getTimestamp(), + supposedSyslogMessage1.getTimestamp(), returnedMessage1.getTimestamp()); + assertNull("Timestamp should be null", supposedSyslogMessage2.getTimestamp()); + assertNull("Timestamp should be null", supposedSyslogMessage3.getTimestamp()); + + } + + /* + * Compares the SDElements from the supposed SyslogMessage and the SyslogMessage + * returned from Converter. + */ + @Test + public void SDElementsTest() { + + List supposedSDParams = new ArrayList(); + List returnedSDParams = new ArrayList(); + Iterator iterator; + SDElement returnedSDE = null; + SDElement supposedSDE = null; + + // Gets the SDParams from the SDEs from the first SyslogMessage returned from + // Converter and saves them in a List + for (iterator = returnedSDElements1.iterator(); iterator.hasNext();) { + returnedSDE = iterator.next(); + for (int i = 0; i < returnedSDE.getSdParams().size(); i++) { + returnedSDParams.add(returnedSDE.getSdParams().get(i)); + } + } + + // Gets the SDParams from the SDEs from the first SyslogMessage created in + // initialize() and saves them in a List + for (iterator = supposedSDElements1.iterator(); iterator.hasNext();) { + supposedSDE = iterator.next(); + for (int i = 0; i < supposedSDE.getSdParams().size(); i++) { + supposedSDParams.add(supposedSDE.getSdParams().get(i)); + } + } + + // Goes through all the returned SDParams and checks that they are all found in + // supposed SDParams + for (int i = 0; i < returnedSDParams.size(); i++) { + assertTrue("SDParam '" + returnedSDParams.get(i) + "' should not be in returned SDElement.", + supposedSDParams.contains(returnedSDParams.get(i))); + } + + // Goes through all supposed SDParams and checks that they are all found in + // returned SDParams + for (int i = 0; i < supposedSDParams.size(); i++) { + assertTrue("SDParam '" + supposedSDParams.get(i) + "' should be in returned SDElement.", + returnedSDParams.contains(supposedSDParams.get(i))); + } + + // Create new empty ArrayList that we can save SDParams from the next SDElement + supposedSDParams = new ArrayList(); + returnedSDParams = new ArrayList(); + + // Gets the SDParams from the SDEs from the second SyslogMessage returned from + // Converter and saves them in a List + for (iterator = returnedSDElements2.iterator(); iterator.hasNext();) { + returnedSDE = iterator.next(); + for (int i = 0; i < returnedSDE.getSdParams().size(); i++) + returnedSDParams.add(returnedSDE.getSdParams().get(i)); + } + + // Gets the SDParams from the SDEs from the second SyslogMessage created in + // initialize() and saves them in a List + for (iterator = supposedSDElements2.iterator(); iterator.hasNext();) { + supposedSDE = iterator.next(); + for (int i = 0; i < supposedSDE.getSdParams().size(); i++) { + supposedSDParams.add(supposedSDE.getSdParams().get(i)); + } + } + + // Goes through all the returned SDParams and checks that they are all found in + // supposed SDParams + for (int i = 0; i < returnedSDParams.size(); i++) { + assertTrue("SDParam '" + returnedSDParams.get(i) + "' should not be in returned SDElement.", + supposedSDParams.contains(returnedSDParams.get(i))); + } + + // Goes through all supposed SDParams and checks that they are all found in + // returned SDParams + for (int i = 0; i < supposedSDParams.size(); i++) { + assertTrue("SDParam '" + supposedSDParams.get(i) + "' should be in returned SDElement.", + returnedSDParams.contains(supposedSDParams.get(i))); + } + + // Create new empty ArrayList that we can save SDParams from the next SDElement + supposedSDParams = new ArrayList(); + returnedSDParams = new ArrayList(); + + // Gets the SDParams from the SDEs from the third SyslogMessage returned from + // Converter and saves them in a List + for (iterator = returnedSDElements3.iterator(); iterator.hasNext();) { + returnedSDE = iterator.next(); + for (int i = 0; i < returnedSDE.getSdParams().size(); i++) + returnedSDParams.add(returnedSDE.getSdParams().get(i)); + } + + // Gets the SDParams from the SDEs from the third SyslogMessage created in + // initialize() and saves them in a List + for (iterator = supposedSDElements3.iterator(); iterator.hasNext();) { + supposedSDE = iterator.next(); + for (int i = 0; i < supposedSDE.getSdParams().size(); i++) { + supposedSDParams.add(supposedSDE.getSdParams().get(i)); + } + } + + // Goes through all the returned SDParams and checks that they are all found in + // supposed SDParams + for (int i = 0; i < returnedSDParams.size(); i++) { + assertTrue("SDParam '" + returnedSDParams.get(i) + "' should not be in returned SDElement.", + supposedSDParams.contains(returnedSDParams.get(i))); + } + + // Goes through all supposed SDParams and checks that they are all found in + // returned SDParams + for (int i = 0; i < supposedSDParams.size(); i++) { + assertTrue("SDParam '" + supposedSDParams.get(i) + "' should be in returned SDElement.", + returnedSDParams.contains(supposedSDParams.get(i))); + } + } +} diff --git a/src/test/java/com/teragrep/cfe_16/SessionManagerTests.java b/src/test/java/com/teragrep/cfe_16/SessionManagerTests.java new file mode 100644 index 0000000..86c4bb5 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/SessionManagerTests.java @@ -0,0 +1,105 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import com.teragrep.cfe_16.bo.Session; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.*; + +/* + * Tests the functionality of SessionManager + */ +public class SessionManagerTests { + + + private SessionManager sessionManager; + + /* + * A SessionManager is initialized + */ + @BeforeEach + public void initialize() { + sessionManager = new SessionManager(); + } + + /* + * Tests creating a session with SessionManager and getting that same session + * from SessionManager + */ + @Test + public void createSessionAndGetItWithAuthTokenTest() { + + String authToken1 = "AUTH_TOKEN_12345"; + String authToken2 = "AUTH_TOKEN_54321"; + String authToken3 = "AUTH_TOKEN_99999"; + + Session session1 = sessionManager.createSession(authToken1); + Session session2 = sessionManager.createSession(authToken2); + assertSame("Same session should be returned with the same authentication token", session1, + sessionManager.getSession(authToken1)); + assertSame("Same session should be returned with the same authentication token", session2, + sessionManager.getSession(authToken2)); + assertNotSame("Different session should be returned with a different authentication token", session1, + sessionManager.getSession(authToken2)); + assertNotSame("Different session should be returned with a different authentication token", session2, + sessionManager.getSession(authToken1)); + assertNull("Getting a session with an unused authentication token should return null", + sessionManager.getSession(authToken3)); + } + + @Test + public void sessionCreaationAndDeletionTests() { + Session session = sessionManager.createSession("AUTH"); + assertTrue(session.addChannel(Session.DEFAULT_CHANNEL)); + assertFalse(session.addChannel(Session.DEFAULT_CHANNEL)); + assertTrue(session.doesChannelExist(Session.DEFAULT_CHANNEL)); + assertTrue(session.removeChannel(Session.DEFAULT_CHANNEL)); + assertTrue(!session.doesChannelExist(Session.DEFAULT_CHANNEL)); + } +} diff --git a/src/test/java/com/teragrep/cfe_16/TokenManagerTests.java b/src/test/java/com/teragrep/cfe_16/TokenManagerTests.java new file mode 100644 index 0000000..61fd1fd --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/TokenManagerTests.java @@ -0,0 +1,113 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.Base64; + +import static org.junit.Assert.*; + +/* + * Tests the functionality of TokenManager + */ +public class TokenManagerTests { + + TokenManager manager = new TokenManager(); + + /* + * Tests TokenManager's tokenIsMissing() method which checks if + * HttpServletRequest's header has an authentication token in it. + */ + @Test + public void tokenCheckingTest() { + + MockHttpServletRequest requestWithHttpHeaderAuth = new MockHttpServletRequest(); + MockHttpServletRequest requestWithBasicAuth = new MockHttpServletRequest(); + MockHttpServletRequest requestWithoutAuth = new MockHttpServletRequest(); + String authToken = "AUTH_TOKEN_11111"; + + requestWithHttpHeaderAuth.addHeader("Authorization", authToken); + requestWithBasicAuth.addHeader("Authorization", "Basic x:" + authToken); + + assertFalse("Token should be found from the request", manager.tokenIsMissing(requestWithHttpHeaderAuth)); + assertFalse("Token should be found from the request", manager.tokenIsMissing(requestWithBasicAuth)); + assertTrue("Token should not be found from the request", manager.tokenIsMissing(requestWithoutAuth)); + } + + /* + * Tests TokenManager's isTokenInBasic() method which checks if the + * HttpServletRequest's header's authentication token is in basic authentication + * format. + */ + @Test + public void basicAuthCheckingTest() { + String authToken = "AUTH_TOKEN_11111"; + String basicAuthHeader = "Basic x:" + authToken; + + assertTrue("Authorization header should be in basic format", manager.isTokenInBasic(basicAuthHeader)); + assertFalse("Authorization should not be in basic format when querying with only the authentication token.", + manager.isTokenInBasic(authToken)); + } + + /* + * Tests TokenManager's getTokenFromBasic() method which returns the + * authentication token when it is given in basic authentication format. + */ + @Test + public void getTokenFromBasicAuthTest() { + String authToken = "AUTH_TOKEN_11111"; + String basicAuthCredentials = "x:" + authToken; + String credentialsEncoded = Base64.getEncoder().encodeToString(basicAuthCredentials.getBytes()); + String basicAuthHeader = "Basic " + credentialsEncoded; + + assertEquals("Method should return the authentication token extracted from the Basic Authentication format", + authToken, manager.getTokenFromBasic(basicAuthHeader)); + } + +} diff --git a/src/test/java/com/teragrep/cfe_16/it/AckManagerIT.java b/src/test/java/com/teragrep/cfe_16/it/AckManagerIT.java new file mode 100644 index 0000000..62ca657 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/it/AckManagerIT.java @@ -0,0 +1,311 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.it; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.teragrep.cfe_16.AckManager; +import com.teragrep.cfe_16.bo.Ack; +import com.teragrep.cfe_16.bo.Session; +import com.teragrep.cfe_16.config.Configuration; +import com.teragrep.cfe_16.exceptionhandling.ServerIsBusyException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.TestPropertySource; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.*; + +/* + * Tests the functionality of ackManager + */ +@SpringBootTest +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(properties = { + "syslog.server.host=127.0.0.1", + "syslog.server.port=1234", + "syslog.server.protocol=TCP", + "max.channels=1000000", + "max.ack.value=1000000", + "max.ack.age=20000", + "max.session.age=30000", + "poll.time=30000", + "spring.devtools.add-properties=false", + "server.print.times=true" + }) +public class AckManagerIT { + private static final Logger LOGGER = LoggerFactory.getLogger(AckManagerIT.class); + private String authToken1; + private String authToken2; + + private String channel1; + private String channel2; + + @Autowired + private Configuration configuration; + @Autowired + private AckManager ackManager; + /* + * Initializes 2 channels. getCurrentAckValue in channel 1 is called 3 times + * which means that the next Ack id given to a sent event should be 3. + */ + @BeforeEach + public void initialize() { + + authToken1 = "AUTH_TOKEN_11111"; + authToken2 = "AUTH_TOKEN_22222"; + + channel1 = "CHANNEL_11111"; + channel2 = "CHANNEL_22222"; + + } + + /* + * In initialize() we call channel 1's getCurrentAckValue 3 times, so the next + * time we call it, the currentAckValue() should return 3 and increase the ack + * value by 1, so the next time it will be 4. We have not called + * getCurrentAckValue() in channel2 yet, so it should be 0. + */ + @Test + public void getCurrentAckValueTest() { + int currentAckValue; + currentAckValue = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + + currentAckValue = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + + currentAckValue = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + + currentAckValue = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + + assertEquals("channel1 current ack value should be 3", 3, currentAckValue); + + currentAckValue = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + assertEquals("channel1 current ack value should be 4", 4, currentAckValue); + + currentAckValue = ackManager.getCurrentAckValue(this.authToken2, this.channel2); + assertEquals("channel2 current ack value should be 0", 0, currentAckValue); + } + + /* + * First we acknowledge the Ack with an id of 0 in channel 1, then we test that + * it is indeed acknowledged. All the other Acks should not be acknowledged. If + * Ack id is not used at all, isAckAcknowledged should return false. + */ + @Test + public void acknowledgeTest() { + ackManager.initializeContext(this.authToken1, this.channel1); + ackManager.addAck(this.authToken1, this.channel1, new Ack(0, false)); + ackManager.acknowledge(this.authToken1, this.channel1, 0); + assertTrue("ackId 0 should be acknowledged for channel 1", ackManager.isAckAcknowledged(this.authToken1, this.channel1, 0)); + assertFalse("ackId 1 should not be acknowledged for channel 1", ackManager.isAckAcknowledged(this.authToken1, this.channel1, 1)); + assertFalse("ackId 10 is not used yet for channel 1, so isAckAcknowledged should return false", + ackManager.isAckAcknowledged(this.authToken1, this.channel1, 10)); + ackManager.incrementAckValue(this.authToken1, this.channel1); + ackManager.initializeContext(this.authToken2, this.channel2); + assertFalse("ackId 0 is not used yet for channel 2 so isAckAcknowledged should return false", + ackManager.isAckAcknowledged(this.authToken2, this.channel2, 0)); + } + + /* + * Tests getting the Ack statuses from ackManager. First we create the request + * bodies and the supposed responses as strings. Then we create the nodes for + * the requests and read the strings into the node object. After that + * ackManager's getRequestedAckStatuses() is called with the JsonNode requests + * and the response is compared to the supposed responses. + */ + @Test + public void getRequestedAckStatusesTest() { + String requestAsString = "{\"acks\": [1,3,4]}"; + String notIntRequestAsString = "{\"acks\": [\"a\",\"b\",\"c\"]}"; + String faultyRequestAsString = "{\"test\": [1,3,4]}"; + String supposedResponseAsStringOneTrue = "{\"1\":true,\"3\":false,\"4\":false}"; + String supposedResponseAsStringAllFalse = "{\"1\":false,\"3\":false,\"4\":false}"; + ObjectMapper mapper = new ObjectMapper(); + JsonNode emptyJsonNode = mapper.createObjectNode(); + JsonNode queryNode = mapper.createObjectNode(); + JsonNode faultyNode = mapper.createObjectNode(); + JsonNode notIntNode = mapper.createObjectNode(); + ackManager.initializeContext(this.authToken1, this.channel1); + assertTrue(ackManager.addAck(this.authToken1, this.channel1, new Ack(1, false))); + assertTrue(ackManager.acknowledge(this.authToken1, this.channel1, 1)); + try { + queryNode = mapper.readTree(requestAsString); + faultyNode = mapper.readTree(faultyRequestAsString); + notIntNode = mapper.readTree(notIntRequestAsString); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertEquals("getRequestedAckStatuses should return null, when providing a null value as a parameter", + emptyJsonNode, ackManager.getRequestedAckStatuses(this.authToken1, "", null)); + + try { + assertEquals("ackId 1 status should be true on channel1, others should be false.", + supposedResponseAsStringOneTrue, ackManager.getRequestedAckStatuses(this.authToken1, this.channel1, queryNode).toString()); + } catch (Throwable e1) { + // TODO Auto-generated catch block + LOGGER.warn("Failed to handle ack request: ", e1); + } + + assertEquals( + "ackId 1 status should be false on channel1 after requesting it's status once. All others should be false as well", + supposedResponseAsStringAllFalse, ackManager.getRequestedAckStatuses(this.authToken1, this.channel1, queryNode).toString()); + + ackManager.initializeContext(this.authToken2, this.channel2); + assertEquals("All ack statuses should be false for channel2", supposedResponseAsStringAllFalse, + ackManager.getRequestedAckStatuses(this.authToken2, this.channel2, queryNode).toString()); + + assertEquals("An empty JsonNode should be returned when querying with an empty JsonNode", emptyJsonNode, + ackManager.getRequestedAckStatuses(this.authToken1, this.channel1, emptyJsonNode)); + + assertEquals( + "An empty JsonNode should be returned when querying with a JsonNode that has no \"acks\" field in it.", + emptyJsonNode, ackManager.getRequestedAckStatuses(this.authToken1, this.channel1, faultyNode)); + try { + assertEquals( + "An empty JsonNode should be returned when querying with a JsonNode that has \"acks\" field in it, but it has something else than integers as it's values", + emptyJsonNode, ackManager.getRequestedAckStatuses(this.authToken1, this.channel1, notIntNode)); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + } + + public void getCurrentAckValueAndIncrementTest() { + AckManager ackManager1 = new AckManager(); + AckManager ackManager2 = new AckManager(); + + assertEquals("AckManager 1 should return 0", 0, ackManager1.getCurrentAckValue(this.authToken1, Session.DEFAULT_CHANNEL)); + ackManager1.incrementAckValue(this.authToken1, Session.DEFAULT_CHANNEL); + assertEquals("AckManager 1 should return 1", 1, ackManager1.getCurrentAckValue(this.authToken1, Session.DEFAULT_CHANNEL)); + + assertEquals("AckManager 2 should return 0", 0, ackManager2.getCurrentAckValue(this.authToken1, Session.DEFAULT_CHANNEL)); + ackManager2.incrementAckValue(this.authToken1, Session.DEFAULT_CHANNEL); + assertEquals("AckManager 2 should return 1", 1, ackManager2.getCurrentAckValue(this.authToken1, Session.DEFAULT_CHANNEL)); + + } + + /* + * Tests deleting the Ack from ackManager. First we get the list that is + * currently in the ackManager and save it to list1 variable. After that we + * delete an Ack from the ackManager's list, then we get the list from + * ackManager again and save it to list2 variable. These 2 lists are then + * compared to each other. + */ + @Test + public void deleteAckTest() { + AckManager ackManager1 = new AckManager(); + AckManager ackManager2 = new AckManager(); + + ackManager1.initializeContext(this.authToken1, this.channel1); + assertTrue(ackManager1.addAck(this.authToken1, this.channel1, new Ack(0, false))); + assertTrue(ackManager1.addAck(this.authToken1, this.channel1, new Ack(1, false))); + + Map list1 = ackManager1.getAckList(this.authToken1, this.channel1); + int list1Size = ackManager1.getAckListSize(this.authToken1, this.channel1); + assertEquals("Ack list 1 size should be 2.", 2, list1Size); + + Ack deletedAck = list1.values().iterator().next(); + + ackManager2.initializeContext(this.authToken2, this.channel2); + assertTrue(ackManager2.addAck(this.authToken2, this.channel2, new Ack(0, false))); + + ackManager2.initializeContext(this.authToken2, this.channel2); + assertTrue(ackManager2.addAck(this.authToken2, this.channel2, new Ack(1, false))); + + ackManager2.deleteAckFromList(this.authToken2, this.channel2, deletedAck); + Map list2 = ackManager2.getAckList(this.authToken2, this.channel2); + int list2Size = list2.size(); + + assertNotSame("Ack lists should not be same", list1.toString(), list2.toString()); + assertEquals("list2 should be shorter by one index", list1Size - 1, list2Size); + assertFalse("list2 should not contain the deleted ack", list2.containsKey(deletedAck.getId())); + } + + /* + * Max Ack value is set to 2, so the Ack list should be full after + * getCurrentAckValue() is called 3 times. getCurrentAckValue is called 4 times + * here, so ServerIsBusyException is expected to happen. + */ + @Test + public void maxAckValueTest() { + Assertions.assertThrows(ServerIsBusyException.class, () -> { + /* AckManager ackManager = new AckManager(); */ + + this.configuration.setMaxAckValue(2); + int ackId; + + ackId = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + ackManager.addAck(this.authToken1, this.channel1, new Ack(ackId, false)); + + ackId = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + ackManager.addAck(this.authToken1, this.channel1, new Ack(ackId, false)); + + ackId = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + ackManager.addAck(this.authToken1, this.channel1, new Ack(ackId, false)); + + ackId = ackManager.getCurrentAckValue(this.authToken1, this.channel1); + ackManager.incrementAckValue(this.authToken1, this.channel1); + ackManager.addAck(this.authToken1, this.channel1, new Ack(ackId, false)); + }); + } +} diff --git a/src/test/java/com/teragrep/cfe_16/it/ConfigurationIT.java b/src/test/java/com/teragrep/cfe_16/it/ConfigurationIT.java new file mode 100644 index 0000000..2895b70 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/it/ConfigurationIT.java @@ -0,0 +1,104 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.it; + + +import com.teragrep.cfe_16.config.Configuration; +import com.teragrep.rlp_03.Server; +import com.teragrep.rlp_03.ServerFactory; +import com.teragrep.rlp_03.config.Config; +import com.teragrep.rlp_03.delegate.DefaultFrameDelegate; +import com.teragrep.rlp_03.delegate.FrameDelegate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +public class ConfigurationIT { + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationIT.class); + @Autowired + private Configuration configuration; + + private static Server server; + private static final String hostname = "localhost"; + private static Integer port = 1235; + @BeforeAll + public static void init() throws IOException, InterruptedException { + Supplier frameDelegateSupplier = () -> new DefaultFrameDelegate((frame) -> LOGGER.debug(frame.relpFrame().payload().toString())); + Config config = new Config(port, 1); + ServerFactory serverFactory = new ServerFactory(config, frameDelegateSupplier); + + server = serverFactory.create(); + Thread serverThread = new Thread(server); + serverThread.start(); + server.startup.waitForCompletion(); + } + + @AfterAll + public static void cleanup() throws InterruptedException { + server.stop(); + } + + @Test + public void instantiateConfigurationTest() { + String expected = + "Configuration [sysLogHost=127.0.0.1, sysLogProtocol=relp, sysLogPort=1235, maxAckValue=1000000, maxAckAge=20000, maxSessionAge=30000, " + + "maxChannels=1000000, pollTime=1000000, printTimes=true]"; + LOGGER.debug(configuration.toString()); + + assertEquals(expected, configuration.toString()); + } +} diff --git a/src/test/java/com/teragrep/cfe_16/it/SendEventsIT.java b/src/test/java/com/teragrep/cfe_16/it/SendEventsIT.java new file mode 100644 index 0000000..7ab9136 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/it/SendEventsIT.java @@ -0,0 +1,182 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.it; + +import com.teragrep.cfe_16.service.HECService; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.TestPropertySource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.channels.Selector; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +@TestPropertySource(properties = { + "syslog.server.host=127.0.0.1", + "syslog.server.port=1236", + "syslog.server.protocol=TCP", + "max.channels=1000000", + "max.ack.value=1000000", + "max.ack.age=20000", + "max.session.age=30000", + "poll.time=30000", + "spring.devtools.add-properties=false", + "server.print.times=true" + }) +@SpringBootTest +public class SendEventsIT implements Runnable { + @Autowired + private HECService service; + + private ServerSocket serverSocket; + private Thread thread; + private AtomicInteger numberOfRequestsMade; + + private MockHttpServletRequest request1; + private String eventInJson; + private String channel1; + private Selector selector; + private final static int SERVER_PORT = 1236; + private CountDownLatch countDownLatch = new CountDownLatch(0); + + @BeforeEach + public void init() throws IOException { + this.thread = new Thread(this); + this.numberOfRequestsMade = new AtomicInteger(0); + this.serverSocket = new ServerSocket(SERVER_PORT); + this.selector = Selector.open(); + + this.request1 = new MockHttpServletRequest(); + this.request1.addHeader("Authorization", "AUTH_TOKEN_11111"); + this.channel1 = "CHANNEL_11111"; + this.eventInJson = "{\"sourcetype\":\"access\", \"source\":\"/var/log/access.log\", \"event\": {\"message\":\"Access log test message 1\"}} {\"sourcetype\":\"access\", \"source\":\"/var/log/access.log\", \"event\": {\"message\":\"Access log test message 2\"}}"; + + this.thread.start(); + } + + @AfterEach + public void shutdown() { + this.thread.interrupt(); + } + + public void run() { + while (true) { + try { + Socket socket = this.serverSocket.accept(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line; + while ((line = bufferedReader.readLine()) != null) { + this.numberOfRequestsMade.getAndIncrement(); + countDownLatch.countDown(); + } + bufferedReader.close(); + socket.close(); + if (this.numberOfRequestsMade.get() % 5 == 0) { + this.serverSocket.close(); + this.serverSocket = new ServerSocket(SERVER_PORT); + } + } catch (InterruptedIOException e) { + break; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void sendEventsTest() throws IOException, InterruptedException, ExecutionException { + int NUMBER_OF_EVENTS_TO_BE_SENT = 100; + countDownLatch = new CountDownLatch(NUMBER_OF_EVENTS_TO_BE_SENT); + ExecutorService es = Executors.newFixedThreadPool(8); + List > futures = new ArrayList<>(); + + for (int i = 0; i < NUMBER_OF_EVENTS_TO_BE_SENT; i++) { + CompletableFuture f = CompletableFuture.supplyAsync(() -> service.sendEvents(request1, channel1, eventInJson).toString()); + futures.add(f); + } + List supposedResponses = new ArrayList(); + for (int i = 0; i < NUMBER_OF_EVENTS_TO_BE_SENT; i++) { + final String supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":" + i + "}"; + supposedResponses.add(supposedResponse); + } + int countFuture = 0; + for (Future f : futures) { + final String actualResponse = f.get(); + Assertions.assertTrue(supposedResponses.contains(actualResponse), "Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be " + countFuture + ")"); + countFuture++; + } + countDownLatch.await(1, TimeUnit.SECONDS); + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + while(NUMBER_OF_EVENTS_TO_BE_SENT * 2 != this.numberOfRequestsMade.get()) { + Thread.sleep(500); + } + }); + es.shutdownNow(); + } + + @Disabled + @Test + public void send1EventTest() throws IOException, InterruptedException { + countDownLatch = new CountDownLatch(1); + String supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":" + 0 + "}"; + Assertions.assertEquals(service.sendEvents(request1, channel1, eventInJson).toString(), supposedResponse, "Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be " + 0 + ")"); + + countDownLatch.await(5, TimeUnit.SECONDS); + Assertions.assertEquals(2, this.numberOfRequestsMade, "Number of events received should match the number of sent ones"); + } +} diff --git a/src/test/java/com/teragrep/cfe_16/it/ServiceAndEventManagerIT.java b/src/test/java/com/teragrep/cfe_16/it/ServiceAndEventManagerIT.java new file mode 100644 index 0000000..1192523 --- /dev/null +++ b/src/test/java/com/teragrep/cfe_16/it/ServiceAndEventManagerIT.java @@ -0,0 +1,525 @@ +/* + * HTTP Event Capture to RFC5424 CFE_16 + * Copyright (C) 2021 Suomen Kanuuna Oy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional permission under GNU Affero General Public License version 3 + * section 7 + * + * If you modify this Program, or any covered work, by linking or combining it + * with other code, such other code is not for that reason alone subject to any + * of the requirements of the GNU Affero GPL version 3 as long as this Program + * is the same Program as licensed from Suomen Kanuuna Oy without any additional + * modifications. + * + * Supplemented terms under GNU Affero General Public License version 3 + * section 7 + * + * Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified + * versions must be marked as "Modified version of" The Program. + * + * Names of the licensors and authors may not be used for publicity purposes. + * + * No rights are granted for use of trade names, trademarks, or service marks + * which are in The Program if any. + * + * Licensee must indemnify licensors and authors for any liability that these + * contractual assumptions impose on licensors and authors. + * + * To the extent this program is licensed as part of the Commercial versions of + * Teragrep, the applicable Commercial License may apply to this file if you as + * a licensee so wish it. + */ + +package com.teragrep.cfe_16.it; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.teragrep.cfe_16.AckManager; +import com.teragrep.cfe_16.EventManager; +import com.teragrep.cfe_16.bo.HeaderInfo; +import com.teragrep.cfe_16.bo.HttpEventData; +import com.teragrep.cfe_16.bo.Session; +import com.teragrep.cfe_16.exceptionhandling.*; +import com.teragrep.cfe_16.service.HECService; +import com.teragrep.rlp_03.Server; +import com.teragrep.rlp_03.ServerFactory; +import com.teragrep.rlp_03.config.Config; +import com.teragrep.rlp_03.delegate.DefaultFrameDelegate; +import com.teragrep.rlp_03.delegate.FrameDelegate; +import org.junit.jupiter.api.*; +import org.powermock.reflect.Whitebox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.TestPropertySource; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +/* + * Tests the functionality of HECServiceImpl + */ + +@SpringBootTest +@TestPropertySource(properties = { + "syslog.server.host=127.0.0.1", + "syslog.server.port=1601", + "syslog.server.protocol=RELP", + "max.channels=1000000", + "max.ack.value=1000000", + "max.ack.age=20000", + "max.session.age=30000", + "poll.time=30000", + "spring.devtools.add-properties=false", + "server.print.times=true" + }) +public class ServiceAndEventManagerIT { + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceAndEventManagerIT.class); + private static Server server; + private static final String hostname = "localhost"; + private static Integer port = 1601; + @BeforeAll + public static void init_x() throws IOException, InterruptedException { + Supplier frameDelegateSupplier = () -> new DefaultFrameDelegate((frame) -> LOGGER.debug(frame.relpFrame().payload().toString())); + Config config = new Config(port, 1); + ServerFactory serverFactory = new ServerFactory(config, frameDelegateSupplier); + + server = serverFactory.create(); + Thread serverThread = new Thread(server); + serverThread.start(); + server.startup.waitForCompletion(); + } + + @AfterAll + public static void cleanup() throws InterruptedException { + server.stop(); + } + @Autowired + private HECService service; + @Autowired + private AckManager ackManager; + private static final ServerSocket serverSocket = getSocket(); + + private MockHttpServletRequest request1; + private MockHttpServletRequest request2; + private MockHttpServletRequest request3; + private MockHttpServletRequest request4; + private MockHttpServletRequest request5; + + private String eventInJson; + private String channel1; + private String channel2; + private String channel3; + private String defaultChannel; + private String authToken1; + private String authToken2; + private String authToken3; + private String authToken4; + + private String ackRequest; + private ObjectMapper objectMapper; + private JsonNode ackRequestNode; + + @Autowired + private EventManager eventManager; + + private HeaderInfo headerInfo = new HeaderInfo(); + + private static ServerSocket getSocket() { + ServerSocket socket = null; + try { + socket = new ServerSocket(1234); + } catch (IOException e) { + LOGGER.warn("Could not get a server socket: ", e); + throw new RuntimeException(e); + } + return socket; + } + + @AfterAll + public static void closeServerSocket() { + try { + serverSocket.close(); + } catch (IOException e) { + LOGGER.warn("Could not close server socket: ", e); + } + } + + /* + * Opens a ServerSocket so that the sending an event won't produce an error. 4 + * Mock requests are created. request2 will not have authorization token + * assigned to it. eventInJson variable is the body of the request as a string + * when sending an event and ackRequest is the body of the request when + * requesting Ack statuses. + */ + @BeforeEach + public void initialize() { + /* + * try { serverSocket = new ServerSocket(props.getSyslogPort()); } catch + * (IOException e) { e.printStackTrace(); } + */ + + objectMapper = new ObjectMapper(); + request1 = new MockHttpServletRequest(); + request2 = new MockHttpServletRequest(); + request3 = new MockHttpServletRequest(); + request4 = new MockHttpServletRequest(); + request5 = new MockHttpServletRequest(); + + eventInJson = "{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\", \"host\": \"localhost\", \"source\": \"mysource\", \"index\": \"myindex\"}"; + + channel1 = "CHANNEL_11111"; + channel2 = "CHANNEL_22222"; + channel3 = "CHANNEL_33333"; + + authToken1 = "AUTH_TOKEN_12223"; + authToken2 = "AUTH_TOKEN_16664"; + authToken3 = "AUTH_TOKEN_23667"; + authToken4 = "AUTH_TOKEN_23249"; + + request1.addHeader("Authorization", authToken1); + request3.addHeader("Authorization", authToken2); + request4.addHeader("Authorization", authToken3); + request5.addHeader("Authorization", authToken4); + + ackRequest = "{\"acks\": [1,3,4]}"; + + ackRequestNode = objectMapper.createObjectNode(); + + defaultChannel = Session.DEFAULT_CHANNEL; + + try { + ackRequestNode = objectMapper.readTree(ackRequest); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /* + * Tests the sendEvents() and getAcks() method of the service. + */ + @Test + public void sendEventsAndGetAcksTest() { + String supposedResponse; + + supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":0}"; + assertEquals("Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be 0)", + supposedResponse, service.sendEvents(request1, channel3, eventInJson).toString()); + + supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":1}"; + assertEquals("Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be 1)", + supposedResponse, service.sendEvents(request1, channel3, eventInJson).toString()); + + supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":0}"; + assertEquals("Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be 0)", + supposedResponse, service.sendEvents(request1, channel2, eventInJson).toString()); + + assertEquals("Service should return JSON object with fields 'text', 'code' and 'ackID' (ackID should be 0)", + supposedResponse, service.sendEvents(request3, channel3, eventInJson).toString()); + + supposedResponse = "{\"acks\":{\"1\":true,\"3\":false,\"4\":false}}"; + assertEquals("JSON object should be returned with ack statuses.", supposedResponse, + service.getAcks(request1, channel3, ackRequestNode).toString()); + } + + /* + * Tests sending a request with no authentication token. In this case + * AuthenticationTokenMissingException is expected to happen. + */ + @Test + public void sendEventsWithoutAuthTokenTest() { + Assertions.assertThrows(AuthenticationTokenMissingException.class, () -> { + service.sendEvents(request2, eventInJson, channel1); + }); + } + + /* + * Tests sending a request with no channel provided. In this case no Ack id is + * returned. + */ + @Test + public void sendEventsWithoutChannelTest() { + String supposedResponse = "{\"text\":\"Success\",\"code\":0}"; + String response = service.sendEvents(request1, null, eventInJson).toString(); + assertEquals("Service should return JSON object with fields 'text' and 'code'", supposedResponse, + response); + } + + /* + * Tests getting the Ack statuses without providing channel in the request. In + * this case ChannelNotProvidedException is expected to happen. + */ + @Test + public void getAcksWithoutChannel() { + Assertions.assertThrows(ChannelNotProvidedException.class, () -> { + service.getAcks(request1, null, ackRequestNode); + }); + } + + /* + * Tests getting the Ack statuses without providing an authentication token in + * the request. In this case AuthenticationTokenMissingException is expected to + * happen. + */ + @Test + public void getAcksWithoutAuthTokenTest() { + Assertions.assertThrows(AuthenticationTokenMissingException.class, () -> { + service.getAcks(request2, channel1, ackRequestNode); + }); + } + + /* + * Tests trying to get Ack statuses with an authentication that is not used to + * send events. In this case SessionNotFoundException is expected to happen. + */ + @Test + public void getAcksWithUnusedAuthToken() { + Assertions.assertThrows(SessionNotFoundException.class, () -> { + service.getAcks(request4, channel1, ackRequestNode); + }); + } + + /* + * Tests trying to get Ack statuses with a channel that is does not exist in the + * session. In this case ChannelNotFoundException is expected to happen. + */ + @Test + public void getAcksWithUnusedChannel() { + Assertions.assertThrows(ChannelNotFoundException.class, () -> { + service.sendEvents(request5, channel1, eventInJson); + service.getAcks(request5, channel2, ackRequestNode); + }); + } + + /* + * Tests the EventManager's convertData() method. First we create a Json node as + * string, that we give as a parameter for convertData(). We also create a + * supposed response Json node as string. convertData() returns an ObjectNode + * object, which we convert to string here, so we can easily compare it to our + * supposed response. + */ + @Test + public void convertDataTest() { + /*AckManager ackManager = new AckManager();*/ + String allEventsInJson = "{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\", \"host\": \"localhost\", \"source\": \"mysource\", \"index\": \"myindex\"}"; + String supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":2}"; + String response = eventManager.convertData(authToken1, channel1, allEventsInJson, headerInfo, ackManager).toString(); + assertEquals("Should get a JSON with fields text, code and ackID", supposedResponse, response); + } + + /* + * Tests the EventManager's convertDataWithDefaultChannel() method which is + * called when a channel is not provided in a request. First we create a Json + * node as string, that we give as a parameter for convertData(). We also create + * a supposed response Json node as string. convertData() returns an ObjectNode + * object, which we convert to string here, so we can easily compare it to our + * supposed response. + */ + @Test + public void convertDataTestWithDefaultChannel() { + /* AckManager ackManager = new AckManager(); */ + String allEventsInJson = "{\"sourcetype\": \"mysourcetype\", \"event\": \"Hello, world!\", \"host\": \"localhost\", \"source\": \"mysource\", \"index\": \"myindex\"}"; + String supposedResponse = "{\"text\":\"Success\",\"code\":0}"; + + assertEquals("Should get a JSON with fields text and code.", supposedResponse, eventManager + .convertData(authToken1, defaultChannel, allEventsInJson, headerInfo, ackManager).toString()); + + } + + /* + * Tests attempting to send a request, which has no "event"-field in it's body. + * When this is the case, EventFieldMissingException is expected to happen. + */ + @Test + public void noEventFieldInRequestTest() { + Assertions.assertThrows(EventFieldMissingException.class, () -> { + /*AckManager ackManager = new AckManager();*/ + String allEventsInJson = "{\"sourcetype\": \"mysourcetype\", \"host\": \"localhost\", \"source\": \"mysource\", \"index\": \"myindex\"}"; + eventManager.convertData(authToken1, channel1, allEventsInJson, headerInfo, ackManager); + }); + } + + /* + * Tests attempting to send a request, which has a blank "event"-field. When + * this is the case, EventFieldBlankException is expected to happen. + */ + @Test + public void eventFieldBlankInRequestTest() { + Assertions.assertThrows(EventFieldBlankException.class, () -> { + /*AckManager ackManager = new AckManager();*/ + String allEventsInJson = "{\"sourcetype\": \"mysourcetype\", \"event\": \"\", \"host\": \"localhost\", \"source\": \"mysource\", \"index\": \"myindex\"}"; + eventManager.convertData(authToken1, channel1, allEventsInJson, headerInfo, ackManager); + }); + } + + /* + * EventManager needs to handle the time stamp, if it is provided in the + * request. This method tests the handling of time. + */ + @Test + public void handleTimeTest() { + // Content strings are created with different kinds of "time"-fields amd they + // are read into a JsonNode object. + + // content1: time is in epoch seconds (10 digits) + String content1 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1277464192}"; + // content2: "time"-field is not given + String content2 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\"}"; + // content3: time is given in epoch seconds and a decimal giving the epoch + // milliseconds + String content3 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1433188255.253}"; + // content4: time is given inepoch milliseconds (13 digits) + String content4 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1433188255253}"; + // content5: time is given as a string + String content5 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": \"1433188255253\"}"; + // content6: time is given with too small amount of digits + String content6 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 143318}"; + // content7: time is given in epoch centiseconds + String content7 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 143318825525}"; + // content8: time is given with too many digits + String content8 = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1433188255252321}"; + + JsonNode node1 = null; + JsonNode node2 = null; + JsonNode node3 = null; + JsonNode node4 = null; + JsonNode node5 = null; + JsonNode node6 = null; + JsonNode node7 = null; + JsonNode node8 = null; + + try { + node1 = objectMapper.readTree(content1); + node2 = objectMapper.readTree(content2); + node3 = objectMapper.readTree(content3); + node4 = objectMapper.readTree(content4); + node5 = objectMapper.readTree(content5); + node6 = objectMapper.readTree(content6); + node7 = objectMapper.readTree(content7); + node8 = objectMapper.readTree(content8); + } catch (IOException e) { + throw new RuntimeException(e); + } + + HttpEventData testData1 = new HttpEventData(); + HttpEventData testData2 = new HttpEventData(); + HttpEventData testData3 = new HttpEventData(); + HttpEventData testData4 = new HttpEventData(); + HttpEventData testData5 = new HttpEventData(); + HttpEventData testData6 = new HttpEventData(); + HttpEventData testData7 = new HttpEventData(); + HttpEventData testData8 = new HttpEventData(); + + // Accessing a private method handleTime() in EventManager and using the created + // nodes and empty HttpEventData objects as parameters + try { + testData1 = Whitebox.invokeMethod(eventManager, "handleTime", testData1, node1, null); + testData2 = Whitebox.invokeMethod(eventManager, "handleTime", testData2, node2, null); + testData3 = Whitebox.invokeMethod(eventManager, "handleTime", testData3, node3, null); + testData4 = Whitebox.invokeMethod(eventManager, "handleTime", testData4, node4, null); + testData5 = Whitebox.invokeMethod(eventManager, "handleTime", testData5, node5, null); + testData6 = Whitebox.invokeMethod(eventManager, "handleTime", testData6, node6, null); + testData7 = Whitebox.invokeMethod(eventManager, "handleTime", testData7, node7, null); + testData8 = Whitebox.invokeMethod(eventManager, "handleTime", testData8, node8, null); + } catch (Exception e) { + LOGGER.warn("Could not invokeMethods properly: ", e); + } + + /* + * Testing the getTimeSource(), isTimeParsed() and getTimeAsLong() methods from + * the HttpEventData objects that were returned from EventManager's handleTime() + * method. + */ + assertEquals("Time source should be 'reported' when the time is specified in a request", "reported", + testData1.getTimeSource()); + assertTrue("timeParsed should be true when the time is specified in a request", testData1.isTimeParsed()); + assertEquals("Time should have been converted to epoch milliseconds", 1277464192000L, + testData1.getTimeAsLong()); + + assertEquals("Time source should be 'generated' when it's not specified in a request", "generated", + testData2.getTimeSource()); + assertFalse("timeParsed should be false when time is not specified in a request", testData2.isTimeParsed()); + assertEquals("Time as long should be 0 when time is not specified in a request", 0, testData2.getTimeAsLong()); + + assertEquals("Time source should be 'reported' when the time is specified in a request", "reported", + testData3.getTimeSource()); + assertTrue("timeParsed should be true when time is specified in a request.", testData3.isTimeParsed()); + assertEquals( + "Time should be converted to epoch milliseconds when it's provided in a request in epoch seconds with decimals.", + 1433188255253L, testData3.getTimeAsLong()); + + assertEquals("Time source should be 'reported' when the time is specified in a request", "reported", + testData4.getTimeSource()); + assertTrue("timeParsed should be true when time is specified in a request.", testData4.isTimeParsed()); + assertEquals("Time should be in epoch milliseconds when it is provided as epoch milliseconds in the request", + 1433188255253L, testData4.getTimeAsLong()); + + assertEquals("Time source should be 'generated' when time is given as a string in a request", "generated", + testData5.getTimeSource()); + assertFalse("timeParsed should be false when time is given as a string in a request", testData5.isTimeParsed()); + assertEquals("Time should be 0 when time is given as a string in a request", 0, testData5.getTimeAsLong()); + + assertEquals("Time source should be 'generated' when time is given as an integer with less than 10 digits", + "generated", testData6.getTimeSource()); + assertFalse("timeParsed should be false when time is given as an integer with less than 10 digits", + testData6.isTimeParsed()); + assertEquals("Time as long should be as provided in the request.", 143318, testData6.getTimeAsLong()); + + assertEquals("Time source should be 'reported' when the time is specified in a request with 10-13 digits", + "reported", testData7.getTimeSource()); + assertTrue("timeParsed should be true when time is specified in a request with 10-13 digits", + testData7.isTimeParsed()); + assertEquals("Time should be converted to epoch milliseconds when provided in a request with 10-13 digits", + 1433188255250L, testData7.getTimeAsLong()); + + assertEquals("Time source should be 'generated' when time is given as an integer with more than 13 digits", + "generated", testData8.getTimeSource()); + assertFalse("timeParsed should be false when time is given as an integer with more than 13 digits", + testData8.isTimeParsed()); + assertEquals("Time should be as it's provided in a request.", 1433188255252321L, testData8.getTimeAsLong()); + } + + /* + * Testing using EventManager's convertData() method by sending multiple events + * at once. + */ + public void sendingMultipleEventsTest() { + AckManager ackManager = new AckManager(); + String allEventsInJson = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1426279439}{\"event\": \"Pony 2 has left the barn\"}{\"event\": \"Pony 3 has left the barn\", \"sourcetype\": \"newsourcetype\"}{\"event\": \"Pony 4 has left the barn\"}"; + String supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":0}"; + assertEquals("Should get a JSON with fields text, code and ackID", supposedResponse, + eventManager.convertData(authToken1, channel1, allEventsInJson, headerInfo, ackManager).toString()); + + } + + /* + * Testing using EventManager's convertDataWithDefaultChannel() method by + * sending multiple events at once. + */ + public void sendingMultipleEventsWithDefaultChannelTest() { + AckManager ackManager = new AckManager(); + String allEventsInJson = "{\"event\": \"Pony 1 has left the barn\", \"sourcetype\": \"mysourcetype\", \"time\": 1426279439}{\"event\": \"Pony 2 has left the barn\"}{\"event\": \"Pony 3 has left the barn\", \"sourcetype\": \"newsourcetype\"}{\"event\": \"Pony 4 has left the barn\"}"; + String supposedResponse = "{\"text\":\"Success\",\"code\":0,\"ackID\":0}"; + assertEquals("Should get a JSON with fields text, code and ackID", supposedResponse, eventManager + .convertData(authToken1, defaultChannel, allEventsInJson, headerInfo, ackManager).toString()); + } +} diff --git a/src/test/resources/cfe-16-test-plan.jmx b/src/test/resources/cfe-16-test-plan.jmx new file mode 100644 index 0000000..882e83c --- /dev/null +++ b/src/test/resources/cfe-16-test-plan.jmx @@ -0,0 +1,303 @@ + + + + + + false + false + + + + + + + + + + host + wsf.cdyne.com + = + Host of Webservice + + + + + + continue + + false + -1 + + 8 + 0 + 1375525852000 + 1375525852000 + true + 30 + + + + + true + + + + false + {"sourcetype": "mysourcetype", "event": "Hello, world!"} + + = + + + + 127.0.0.1 + 8080 + http + + /services/collector?channel=CHANNEL_11111 + POST + true + false + true + false + + + + + + + + + Authorization + AUTH_TOKEN_11111 + + + Content-Type + application/json; charset=utf-8 + + + + + + + </GetCityForecastByZIPResult> + + Verify content in response + Assertion.response_data + false + 16 + + + + + + true + + + + false + {"sourcetype": "mysourcetype", "event": "Hello, world!"} + + = + + + + 127.0.0.1 + 8080 + http + + /services/collector?channel=CHANNEL_22222 + POST + true + false + true + false + + + + + + + + + Authorization + AUTH_TOKEN_11111 + + + Content-Type + application/json; charset=utf-8 + + + + + + + </GetCityForecastByZIPResult> + + Verify content in response + Assertion.response_data + false + 16 + + + + + + true + + + + false + {"sourcetype": "mysourcetype", "event": "Hello, world!"} + + = + + + + 127.0.0.1 + 8080 + http + + /services/collector?channel=CHANNEL_11111 + POST + true + false + true + false + + + + + + + + + Authorization + AUTH_TOKEN_11111 + + + Content-Type + application/json; charset=utf-8 + + + + + + + </GetCityForecastByZIPResult> + + Verify content in response + Assertion.response_data + false + 16 + + + + + + true + + + + false + {"sourcetype": "mysourcetype", "event": "Hello, world!"} + + = + + + + 127.0.0.1 + 8080 + http + + /services/collector?channel=CHANNEL_33333 + POST + true + false + true + false + + + + + + + + + Authorization + AUTH_TOKEN_22222 + + + Content-Type + application/json; charset=utf-8 + + + + + + + </GetCityForecastByZIPResult> + + Verify content in response + Assertion.response_data + false + 16 + + + + + + true + + + + false + {"sourcetype": "mysourcetype", "event": "Hello, world!"} + + = + + + + 127.0.0.1 + 8080 + http + + /services/collector?channel=CHANNEL_11111 + POST + true + false + true + false + + + + + + + + + Authorization + AUTH_TOKEN_22222 + + + Content-Type + application/json; charset=utf-8 + + + + + + + </GetCityForecastByZIPResult> + + Verify content in response + Assertion.response_data + false + 16 + + + + + + + + diff --git a/statistics.ods b/statistics.ods new file mode 100644 index 0000000000000000000000000000000000000000..cc4aa52974497b4192dd3266e2ec32d4bb3cdb95 GIT binary patch literal 20883 zcmc$_V|1p=(lDG%Y?~9?)=X^Mwrv{|+qP}nwrx9;}ztZfXf46OAXEe)+4XzXq540R3cO$-ekWd7m_4i5eo&o4Ir{{6+% z&_S2R#nR$e+?drG9kRgFJ0$Of((ag@1fP-*O)KQ|H%H$~=#+NZ-bG<>0x|mJB(dF8)IC9x1B?21&oaE7^gLT<-h_6WqQ$frIYeLIod6#Z zsp(!=q~|R;AWp^9GfkNE!Ucv6z_L+#bkwSeS$%(oV|iQz|?kLQxL`j z+_W<=!X^37CIu?+BuYfu^n*7+m7<}j791$?k?(!Rr4RZ_51z`ulkYxP+^u80^L=Gv z;O?oU&kxQZYNEhp#|-B6J?WxFmsXdhi82Hr2C39)LN2erF?8mbNLh9BX^*{}%HnZ( zU>$`nvX6zQZ*rThWj=MWoV~jxXaK&&IrHP{Jzt8K{Te;i_@u59xWWb7ctt4fk`V_3 zFG`+uN&o}^5CQ@K_*YZ@(>?y(j$awROo^8QMG0*clj2#7$WF)4>N^aRnMqF8`zj0kI<+Z86L0W)4yTp}kE- z-0X~uQ50Au2vEPGGR#qN+! z$DDsIm(De1j2%(Fe`c zC&q47y_0~&&&fRF*XX1ATRcVKsz07n|70=?^5r@H|Mr~!WIm`b^XXe#Is6$lzsA7{ zb@iw<7F4gRDmo3FEr;*KV)1DydW-{lGb9@XqQ0HfCZV+rF#^81LyRvE?qsU9{WV1v zSSXYJ`f*2`%9&7xQ)yROrFIX)hUy`&R=+g7J9Zr@YI8R?D!kwCX@e`fx;_to4c`$Q zq)Frlhb@hY3RY}?qGX>r&53OGk`{81fT>yon}<8e*M&7#}hvvWlv#LU`5m zbj4HXu0>x|tUhoWpC!9;l_#{TFsO+vQ++~#=}x=kfFZ;1`3^730L6#F1(77@K;vo4 z{vz&TKO?E_fJINLQy2p%oe2jA_wcNnG0jG``}Q*bQR&*jxQTAFjcw65*g zob{n;TIIlt%yQ(&Xawt0vWw0$M{_?sIv7qiOs#lUL>B{j4O+<8MV6^>Kv=<4o%HJF z_lTDw)Zki%u}tQ;;}*&15TuDkHRCEXHFiZe!Al0u4bDUZl26*4NfXFi24A^bW7dX*;X46-!B zK5R(D(O&rWl3A!)B~@g{K(qbuPKR~>UidH=h;0N+YE16TkePV#p0?StcU$^d{?MV` zMTsw$0azAQ9+U{iY=mDCcKc%W4!0Qc$mw6Htx;uE&fHPA$IJ%cH-j7Am?6s)D3+7L z&7kTq`QRnYTthp53idH<)(AB2xB<4mU$#5^)?*Mtm|QoiDq zx}*7Mz^ba=23a2dqqR3Ukuz6Ta6rsBOzF1!;z8-WcC$|p1SCbgNSE{SJ_OHY61IRL zA{BkUDWcpYEtWg$p51n?0P|3y2ob}Eq9CvFvGj5uO3^u%`HcX_k=--XEbokjE^e2x zx>yjV)j`}|-ZqvNo@sdX1B7IN|+qM%vSH@|DCS8|yLE6+bL@OwUo`EkLl7pTdEZ zWmX>#fbmk$EVoX>em@G#qx_($omK$RPHLRhPBioIS}yR>vqhS)-a z-rVVxjQ*F=kw^V0R*t%XlQtCeG2uw71ua_GwX8OxA#u7W0q|&Xz%Wo#tsKw1h^VdW z52TiFKiG6S#?^`myq&rifoK+}f}Ee*QQ^X^g?pO_cho(! zupsb+dV^!G0kf9$(CzQDJ66PDlaQ9)q^syL7Z>b-E=|KrPY%niDmKnaqWbQWVZ^Bv z!KzQJs85~!4D~6PUZzj7{Y;hWb$_mvf;3U!tCqgL91~tgM9F zY=2uDD+9Y&rc2aW4HnUgzV*3o3a6p;`Eh@el(6cY0Q!5~8V>dc)bCe{_AB=tR0fKT z&x+U|KA-Z$_q*Pn26?{5{U>LjG_)Y{XDgAHHfWY7ro!^m+D_|+JAk%?0R&l~L?L>5 zMd5&IBFqt*ekQTcy6Eun%OyrWR@BSOqmbL?FNe(_tYl%`og_e5+1CPla< zm!cX9W;Y@qK->?K1^*sS_REqv&EJZ8J|+QWMhKjs$rc`paIuuJo z8YgP1F9GBp^Z;2T5TB>GCiE9tWQaI_Rg(`$2`nHB3EXqynl@tkaLHi$ViJduFZj&}H~r(X1yrX9ila^G0kCLH?!-qGu^| z@~?tWqiQ3+hmJ<%BmSPq*aG;c*2nw;Tz>uC*h^pZdOgf#6nmulc~)jXyB!HZdgA!! zM9C?D4UJ~%E*muyTu9whKXZF@x#pjJ%nP?rG3+KaHefbOSf49F?x25|p_T+vkO&SJ zh=zp1JsGc^v+#xoa@xwm>mm{&ufC1|y!Y5hD}ydLCfA+N29f-TOMe$LRXO(_V)9`4 z96$W{{1cSqYHW3Ad@a0+82<_9|5H!``4yDdJGfdH+W(13HqzScUJ?( z5wD42xrh2!Y`kGYn>Tr4n}o);$mRZ&v#r(c+DoHSjm=||tet>J4fD8kd0NNWD!&^` zOLtpJhUyK2^5zKjfJ37f7m3k)?&|ntVY{PM&PuOiBBs|Op{lgg82&k|9u^4J8>o_q z4P_!AcUJ6DV;}-wLnRYAAU7zkwA&j>)5ajqU2Zz%5<3m$O=#Ex-FM-{xiU!_VZ1{X zcBqR=B9gnEx_tpfYFiiMTGj7RqY}`%#EX;&SDYE-1Afh%OoXl0pB`^l&JVibKy1i` zM_ExVxq>}1;UjIlqkcU9+5X=07?F3eT!W0-yM|4(b7#>0%w;+%=E0as6lghm%7i&l z{jEu)7x5R0(sCh{e)*wtkECCl0~^Fd9lv~|eXOw~Mt(U3VqCA0g@?SJlmc3=xH=;Y z4E0h~-AtsmG4J>-qL5k>hR%r!4i!ktij-6eighR3}`u3e> z&EK&80GMylX9#SEKG!xptk19&oVnNuFF&eSp19G%)I|E1iTiYzDKbUG)ylmkRUxDI zfzo7rPiaanIv7M`iAn|*&6&{@7v&wz?<@#lp|!IPeh?}VIv_R^spE^lPjZTsXFE7! zOwXzUwH<$Gv~5}&+D3s8)LbHlpn^k8+uRoDE4p8nA@`fx<_g)+20@l)hnlU%E1@=i8RPZ1|P> zBp6X@R{!KyN2uY2SObj~`8KvK1meQMoyHgO_G*pwzLN|rcj8MiA`q(Z7!Aw_Bv`+W zKV+baQd91bXtI_~6S7+$`HkGB`VhT`5Beicr4_dUxx0~ZSoPbfc!&b!aK1>jn&bQl zAEm6*vAjrFaX0f4&CCFem!}ra`XT;9~rDL0d~WH z5$bShK;@>6fOtDn)SSmMztEjZg6=sUrb~t_M$RY8nID!PsBfQsW_pf_<}?jq3=(_&s*>JC>Puk0-`_oG(qD z_b*18GJ=hLi(Ck~Ol?Xwz*@$y#E`_4ZT3e#Tgs|Co+_~iOk9c0UCR!xsxC*2Gd;gRd6Ky|_sB5pC;uU&4w> z?y>8FCKzAyZAM2DXat273F10V1_<6n(b245ic(KM>Nm82eRCUVx(su`=0aiH&+3Q)I*!43W=dmerUkF*E#@l=3<#j^UtPE6LydI{sT4Q8Z2F z?zd}j^kPT_iKe8x5|Idp_5+sP#KRk;lQ=V3vrxU<(!mTBWbPb3$DOh)^63^?trFSx zDby4Ow3fh(+~=auQ-SPS=g}DTkjpai^j2RvYIvcKse8T#^jq1FbMFMZ2e>~IID>nn zWhfW`Kp6Rdo4~QYCU9vzGedm`IYS!@U46r^eJTfg+JAxY=`S+OEwGhQhOS-}*UYOr zRNeJCrP^!Kgy&FSP=*xKz@6(XHE!u@LQ-Xb+Y7FIqfHR*Aw!AzJYkL?Zh+&nP@rsa!(8ca%s!ZDqMtyHmd8JyqXrf?f&kb~W%xbRT&Zsho*b z+V}E{GQv|<7m8aSoY4@E$$bstXCAS{cHcuLFSTY<_yXU^MGy!+zyzDcuCdW5$mJJz z?#M{mVF~L7{2P1;BmWP0NQGW7>=X-jfvCwgoh)BO*0V1sb)N<{;(EX>3$D)rDVz|W zjfbY-uYxocY`~rw6{)|fWvyZR=C|hy8HT*TirpGIfB=sjB|t8xuw`>hf-3yeh6^w6 ze~4~2%>EZ%m#W17V)3KlFWLW9RpR1bqD@OuMfz+Xs|o!Np1!o-UkX_1*#ZL()&2Tk zM4gVDe<`uizY7i?2}Sw8r1)*m{69so?MNVm2b{W|Jvo1=(&L={9LCQ&qKRFy%ic_? z`A4OIz9;p2$1k;ivUzyx@%{}0WBViCA5f;?-=HD)e?Y6Ee}jU%{sEy%{Q>#;R}KHE z;06k8?MDV8hL`C+QlxRtal3ymd)cw-w3J{5rEb%?OheeBjmAutj}p1r;kEPARZ3Jv z^sBr~xiPc-bKu+4J&vT@7U+PR5@YZZVtvr!fgG)m9(P$vpYD@au89A)cfI`P`I*J}Ts>xo-LAR^W+iKT@uVsf z*le`>74aOc$+y5|DL_YoKgir@q+IWPewYMIP5m{C5GLc*(pA&H=yXlxP})-5uKGkWts~8Co}|7)OgKj)dCX8}3rbw40<&5P ziVA1>zz(8Z5@^xLw*V-uIJ$e4S?2;(g`>Kk0K%dI)S?0;5td4o*w@jdn}e#_LC4<~ z!Xg9IBI65uZt@Fwu>|m}zI)a;qcpDja>i#v-&Yc(5%FI90T{ZiDZ~hRF-6D#jF0VT z-Ux^Wk#Q2k0K#mV5Sh<)78C@iL`Q!>Q=!Qpy^su0)1 z$Q7FT(|5;ln{oP8joNKju-gbst&?%6ZAa5pWv_8!b~1VV&O(iCxoQx?{f%jPjrwtA zP)kv8nX|BGQ@3V{l?;8bf)U@Y5HLM!2_V#$$cGZFI#V_2C zJ?Xb>7DzZxaZBGip0YpXUGWNy6Zty3N`n#R+KP1+2zb8|wPB!>Q!+PPdej!zj| zk7eJ`rQ1C8$k|*HyA4>;H8?%|SBQ3Vcl@-OEHNnN*m6ce$ zBdvE+(r{DqU{ey|3jC`SWm7U?Q&RS-P;gU{{;H7Ts?cnz5PM>}?iu8+Ih!__PG&%| z3nb`_4O$a>*12z*2jGq{+BX=uQ6Fx6Q8^pip79ZDdkMVE16Hgwy7m!1B2ow)^y=q} z8a)mBgxdvLUxgR_`0H`j=i8gQ-{aF6y~l@k$VIjArN5^w>v>vUweqa2(T{Yj`uvN} zg-OJ8shf}l`BUv%L$Pijmy&CDu1$rdR?^y&ua(T z;hnFOuJ@f{E*sMFVNeb*Zv<@p#yooTS@}(gFVly3r?=R5o0N^9vU`B}9ktIPEA4r3 zW1bC~ALIy8ZybkXFS{*cmHS&#Pru2BeKV)P1JgSY8K;hL3HaQ~ z6~4s>u*K02#sCWxvV> z@72}icf+}(oJyG)0Gl%r!&#|ICwlctvIb>BV9p&Ik>mY7!g3#CUIeSsawd@lu=eWI z@#^%XPMmbb0%%^&D-!m5^fbMp0@Mc1b2LeL@%?w<%c2|W^8q3n)T!@99vVouqP(Bn zE5ge3SW7+kK2#~1fZv)3Q34AopoqFFOHlXI*m+;k#k`7>VIP@yd##dKpi>#j(2eRk zCx4BF5qdb5+EY20Hf-*09!rJ2?ubKK&58vh&shN0=zErm6it$Ynt|3||! z^=LFeX`P&LeDB;&H5A?_jt;3$gJ=5bgVO1TkC!zo#b=|Ry(9-b7Tr^6e$6g%v&^Yl zdg=qZA=dBdNpcVq0OrxxId=roNYm+A_PIc}+`?Hjwn_k{^1eQKA|lB0@d~CNpNh!m zQU_=*u&-AfIYVT}X!~Jwqy`G_p*;JnI)S^uk^VCCjDeYVUU)ei_V?4?K+pWrGtBwN zjJ-+3J03k_oN47X}?c|1acEb@~rI0=p3?q@u%FSi_Vr}U3C~>Ly7!HF34x- zJ$iU;I6k3>{6GO^OrYbYTN*hx7hq$o=HYyxmk+J*nD`t03S5<3dXVc`4s;_Twy78!MEXW@9!*`-x^kDEttP8ksBHE zizy?dh*Bpu?wf&}j(HZ`_>**S`ObIFgw8#}pxFQf|5oM$s*tEx;Tw~c(q!F2hO-2w z#i&vruSwU>2|6PODK~0Uww&Me<`f6PM;)Iz4BiR4%f-L(gdl|jTw_vbI?GPodLwU( zHfUmOY`rAe5!NUM-W3ZUGrHsv{Y361bM_pE>**vMvt6W->1CYj(UYrkyAJ|zb3Vm_ z^A=gXkF65kp}M#6{){ZyCwI#DzyJUO`2UP7|J+RhU52w*Mgsu&lm1-5P%v?{)U(nx zwXmmk_$NzaV`UsFBP9$2iTP*08%9(_KK;}%sGW}_0~ViDry z7Ukts5@b;j;*b~NQ5WYD5fb4SR}hp^5toz`kye$EQuUcQMv+v(R%jGqtz1@~|;NabF}kxwRd-SSF`lhvc^bEA}O>p-OwfBp&3rukc$#4yda|ufE z42^dWOLL9L@r*9;vC<88G70o>i19R!^|B51b4&2GObT+&`Q=s+?HLpl92k)h7MBtq z5f~d8oe&+K5fhjm8=jPu7@SlbmRTF0o)?+hn3h)(n^zZG*p^Vzky2QfUeb_O-kuZX zn-}Ysn;2G*5>cAqSCJf0nii3to=}q>Qkt96nDeWtC@Cj5FSnp1uedO;xV)qwx45Xd zqBy^_w5*`KzN)UOpr);?y1uNgt-d_3uClbPGP||5w5h2vxx6R6W+1PjKc{gtzh$hn zp{KlMptOCgq+_POeW0_aqpEwVyl1YpccgJ>v1MYnb9TSGEU~9Db)Y7ruRgE0v1F*Z zu&GtgI{{Hsy<>C49;q~{+(zSjt;FyBc1wYUf$YebSUZodW5=-z02 zEv;G@%iAqwfph=IZyoah)w(R zk&ep~&wfC9$EioT$aQ4tJQEt|oDDo?$PPD3Ri4*Nz2PbpT2T*vd*SSMJ1Y(M%teLC zxyu#B%42WrhxgL$LSU7*JKf5qe&wj=krN=`kXVulIK|?p3Isi$oas@bf^D=W*~$B* zZompe4%_ku1pIdZM8E7V=<&P+^7wL2!G+iub^ZB~!iUy)xzNLxU_Vz{FK%>wzvH)+ zxFw}aEiNfcNH2^!oadnu(u(kLw~T&V`P%U!nJU(Tqw=hwdvP9`XnYBBt`QXNkOR=9 zeiK}Gc_;^37kC9Vd|Tset8LX0(} z*D`ecDfvI*45Sl(tj0EHaeuB)8qW7FqVY{cx?EC`2j<+8KAP~(RD*aF5+aOn9Ul3c zDWv`?IKe;CdYzE7qO$^`LptW^jDu_AMEveHcl#vHNUxU4C#RJJwz-B6cmTE?1(R&H zr*nk*IV}%&RJg!$;81Z(4e)^dQ5D$Zj&b;#M zI-cbLJDR&IVVIDZWIwpoFOO)PNMg@NX4Vk|4JpJ42N(za>l+D2{KOJhVOGq#J1RS? zL}^(hdR=W^_{pjgQ20f-to5hyb%6v=6+UN6Uz-P-cTM<`0KnPIe>4Z7}FBIV9 zc{N+DuQ}=Uf{uympJwIIt2eYNlMO*$6xrElJ72-;jqnUbk{lT(s>n{Dso&y!!Nuc5 zBU_QlDU@9&!i-~S@zE+7%Y!q70ZuJgQPYSe6Ld;RM4+I@v_uu-&;%k}r6JGJvCCyD!J9Bi5 zdMGd$e21~;6c@P76UT%4p+bqR1xotkno)?^&Vv$t1d~*?c0-|s$mUFyUT;(4?(VK7 z=kWa7%RGJf?JX@U8{i3kCgeFwocy^YIpLHL#c&vOJQ`>g65(M9Gp7CXm|w?!fmjQ? zp^+)TJ)=eOt$_*CWn3%IJO>6Kn=`qB5v^?Mv7M#qZv#zAUtgD6optJ@N{0&n7f-(vpHE_V~3e3hC7G zX*cTd3Mt4=)-5UJ&9er+t%6xV`;!*ysqQ_O(X}O?nh1DYD;KIpr5!P!v6kn~dj0zg zq>X&__TS$)hIh9aPpSU@NdaID;zQ2{yarr&KYdN|~oTD`ft_1JeT2O4`+Yyc|8Ptf^ zy`7q2M+Ss@W^Wxqk-bR-%4OZw(&~;Xu;UtUvC1f{FFgCPmEm6z1FLIkJqViH`{cl7 zGb)+voQ;jWO(R$ry(Nd)jc$mwy#$@w{@?FC*7EOvxO00D4A9Oz4zJPP%5>l(;E zvMn5X9iK7rFiZ~jSwn*ADFG7mCTpQr2+$xN#|2iap<1<-w6s3EE8+uAnh>bgI!Q1h zxyTaQS0P$?bji^zjsx4hhgAyiT8U10R;_?~14Y|>m%6+{+O z!3D{+^eBa4;=a0&4&cDYD~RdvXQh#(bVnhal<8*jIN^juij`ReMSBnq#q*Yvgtaf7GkgV;@1S15vBvyFyN&9D4HeVn@4Mb5em5R!NY({&!Jz=UZ34X$T%W z64IYSpI~cV?;-M+_@;WOCE1O=VfUIuGqghE8yWa*Kg>_-JhTQkX1((5vP3oBaCpB_ z_%+q*oWuUK{UOK8<3p;?ZVM`UDFzL4*CP7vOx4D)kM= z95{Z9Sxn2~d>=ACq7Sd)3q2H`P!pB!Pu9mqvC)3R zfs7t36Hja}d%|L39&aZ!9mtXSIKQkD9D6`hm3xq8&;~u=*-tq*J^*o{jPMPE-M;-x!QxqyRe zoL`m{_RCGM80OJr+y|<0Nfb&?8Y7^+L)C^B%Ai&xDRVS4;^r#}j0I0*BCip1F0q$e z+s44cFmyp{k0b!27lFLKfiB?y9GI-C6KVxdJI^bnUB9Hjd|61zKs$sF6ZLrWuNueKle-9lTXDb^PkZw~)ZQ zRROlP97~+h@u!^b(2DJ~H8mokaV%3%T`u_>&gpn7vRQcp=l5Wwpn)~#N`-qYsDlou z2^1^A7K3&RS>|JJD93mYwRq-=Ez)xgh1^c_?zi8?3K&UenAzCR-^$0B`}&}dJv7N+K12q1oZxBHtttB;^3=5%1cEv1cgp-khae(S zOXsZFHEgS!1fx@p8|xX^h!d2PvPyr6!o;;`0IKq{;JY(GmVs87NoXEk>WNHL0^%X+ zsJWGl-9Cl!4hPi`n05=Gl~OogaPYB8J~7>}MD=&TXM!R9-BDC#E-+dePdo!(Wor1T z4TLPn+MzB_iVkL+?eu!Z*YE@wkd-%a3h9a0fXg4>TzX5ZZd_wYiC|Uz8+qNSI-L6r z5cs@zd-p4-twBjDdev}n>+!At2RR_)L;zfjbb(DlDOYc#Y*dYPYFW8fYP(o1@Oqf3 zzlK%s_k96s$)xOc99Yt7GFBeq^hZr#FAoy83_O^h22AkGKZ}NNA~9xkG0`Ri6?&^H z^j8xRtP_!ylns|4f7QJz$4`V=nY@9~l&ST_PPCF-oDnt+zmCTJSW*`V9wGOGM+}O; zoXjsI$ik0eGd%{KMmt>AQP}b>6R~BS)XBtWD&UOvl`Xmi%!Ce;1(~3IU}xX`b-=@) z?9|8OgK|X&41It*2@!oo9jwQJr?nmo9Uz5TSt1@VMM5z_b{L&T+MBh1r|=9n1*uCH zL>5qYf;r)AN`71+=TZZSG4F^kAt>XJ-0nMEeF)D1+g=)xNsm%~eu7`SS?^E@&s z@6HK3Zt`09aHO??RG$Yq5O~r=Kr9i1B*+-$61Ac40`M-uHThtuN6M!i<&4e4$JCc) z00zPts!IXl$;Bd$RlA3kTn8i z+ytbMc-a(j+n25dJcd#Pr#-IZp@1ngC>LW zKzZin>~;Tyu7}z`Tph9HzmLZok2(;NGNIJRV+wHL-_bkGS_9i{jIPF~N&`P+KX zpLI@Uk*N12O$1kB805i8N(+c9lwv+fcBRv0rR{k}E)a%=+Ojy{m#~;n13;PIfG~e} zcNvP|y^o>iFiLHC()OeZKEZmfjWYkxK<~>p?|jk6Z^@aWtL+A_=b+#T@rc*AhO(vE znsa>0GlgHidByeZ==Nx8D~<#oMFG5vB>I~il}dGX-VMHhwjSvq{>bLxp?QaU-~wxq zno$oz(BtcAOpU5b9hgde;Nzo@c@@@5t4=jkj-gs~h+@T<+fFk>G{?#Dgp;${7mr6LOzllqWIghvk`% zLzdqmBwfHcUQ~ECoVns=DJJTlTf)Ve-?59B;mR;juZ z=)l^I@jVuby$*VgFmELrG6GAb-j;Vz-s@3sPEvqVtxxi(|scKRjc zK}h#B<~P*j<;WCmBZJEJmd^D`P>O0&PSiXY*=E4uGQ)y@Mu(%D84I$7vcnZW-_mHH z*pqZqRY;>tU8Ml?oyt&G?cD2jbujC&W%ijX>M^pi;!df&yq4Z}1ce?{behJHCRIBo zr8YezlJH%>6JGOk!Jf55Qe1D5W_IX++Kr0P(Dl(U6Tdl1ut^1M>*JZS%5ag?+jV)_ zkZR&o%5t)_;5*0#Ox0N7junSZmI2?h%g|-+;{H6hQ24Rf+{_uci%>Hy9Df}h4^Yeu zhE47}vH*5(J!?+*07}O(_Cfbf|6hHk%MCICY=?+gLZV=dw&&W(2UPdFbfKS}d$CzO zeIL9dzTR}oirc8CSRTFgqDlXg7zqcDOXaX3$a2TUC4;mePx?F*r6bB(yC{U!953CV zi=)?y3CS3X)y+q;VQ`l5iZ|Mh*mjA%<4EsrG;5&ppm*51;XSi%Ux41(?Db7nrbpmD zK2erRdeK-U=uw?tZ=1mY8QfPm*4cG3%?;s9`p#4Mfs@srX{}I9;|ZBqS(y`!0OD(~6@-TaF&&+uNY;F(W2GrXo zSIyt^XBqhw)`b4qQ~6pS{J;G8{E`0&815K=si*@R{k268ASx&&P|2s`_kVtLjP!-| z&owyO|GDFyksw}~Mu!}9q$wyc)DN##QWLwbie}^31~7yjqPXX8fz;eqH9?t8GJgvH z`+ex>VoqIoWCN!jp?FfZRXP`kEV4!Bfc^F1hVcWTL$foznhY2$mC%|(hK1!(-fxAy zh9Q0{xiQ2g(rR%r9=@CxCXD+u7CV;dJ2&#LFu9|a^qWS@Crg%GQ6aHd#96>OE+_tup*fzZ zh1#mPG*h<)i+slX4Vju7V@K#`LSsC==}6Jrf?Po|Ug72DD(5fg=~!5e%7=WuX2uGE?v;S6l8 zeREbPhnu_6E}!1<%u0flG4AhB)A`|O7g`&!V6HPvif`!C6E*v;PIna32Mr!qetWhf zd858bWva_FFQ7PF1GF?;E8A4ItxY^yHB@J#v2FFU$9W`#UEHw8;-*3kG!*-vaZQ)q zx2%2Yf3!zbQk5qmn}0TBaWm4Ylh0o*jV)$P&VBzW1!dFt>cEdbm=GFh*U+6xGw)xN zXLo~)lRI{Q2|WeVl0JMFe0=-YIkZn_b==n1d(2_be@=`3)6<8q=kNd1KmOOl%nR%_ ztG(uj4sBhR8DY`(5M)Cl)pKZ>dtGDMa|z#0lrVv{)8X9jBQ3JDg(w`yCJnRVTAH6*$6@0RNAAq2!I6H=>(NyZSVMtO1J zZV+0^B0R8YAu7h)#gFbu;|!RpUyzX7pa*HeT*!{y_s%T8EhTAp4XU?qj8b7**+S+w> zj9?*~OueDRES*l~-IP)NrU+*L#R@JRHyBmsti`yPc{+wM1J#npvSJv&{%AIHbX>Iz zYPU*0#nE=@O=mV_8S`&`f3ROzGzv^+*qDzH)%jWEeIaJ|glvnqG!1w)B2t$nvPe<5D5 z>;2L+IS;9IiSoxet|5I})y@hrzRPyGYpHW`~vZ!zpw%k|zRYIo=V5fK@ z9+Hii>2&1{GdB&W6}E}tEEKYC2IJ@2i(2TX3g~X)La)Wc;>`#Gnp2(ra=vQyIU|pP{naF%d%w*<>l|;B` z!oR`26+u;`7zY zP$V5?<*h?NK8*q*q{#O@l3_I$ z?g!qdgh@j5efO@daRJ2}=v_f9G9J1Fjlq*WH�!*D56(*U+s z%&jE<7a$?BPr5*w_S}1-@+EeUw9%eV|5tCW;=qe+*dJO7!PdCZm8}jm>JgiqXFnm>YTy1e>b2GI();4JnZR!K!ZN+LXT4?m}cq{w)^ zDG0ml23#f(3r1)6-!Gx+2D}WTz2zMcuH>adCeU8GkYRqsq%_z?>bVRgq1-W z^|04uzgo>nPv`t~INrY%F!!Og(OsNM`i;ywQ>am=%#{3$X9eIf#V-w2!aObno@?SQ zsk4L~WE?(POH@w8aC4e1&CxC`P5}`m>7@|*{^}N z)$#0g8b*cgk;fThDYc1%tU#$o4n!fU*pH8ZHQQ?_pg8~xh@=xQA{m@@+hRPR1#mH> zqu_peC2E3m1@mRzbUoQZJHHM5aDA?RXT>4p1>fhp{@2UkPGsHR*t%WJa7tz!#rROU z`R&N&o2ve2t6b9NcU;tBbQLf}A||g1ZSjFg^9aU{oxC0ivYvTlXfhF~1rdsw8q(R4 zF(h$d!Fl^zf}j6iBj+9t)z-)HVaOykqh93}GcLI$x#WH@L~<9IGr1HagQ7W{Nr4g-{WpGh&o-Nm3n(GgHr;J-qb1zxikO^X%XES!?aJ*IIi$ z-}P~}^t|mQnU9w`{MV9|CEwXs#+HM~%tsY=@H-|-i*^+Gy1eQfgF0qXL2(B45vA5< zzPOGst;o1Yew&|%%I}cwdKb}UvXC8_T`;H=(NY)V=S%m?Z9lkBO&-MNKSw{F5{-Mq(+R5(`zg;`{HVr2X`(I-M8w8N#E z$NiKX=gBYR46&F4pQ=&NQ&kpe5_~7HjGZ1seWYE7^vZf2R^o+rJ3AQpx-KFx#WE{j zhiI2-_C8$rP#)91<&BSA$7gTywtDeC62))b>>@aZW1KH;B~67>=DG4^R%F$+Xq(K*mO>chA}dh=7H zk-F5$eXUvYNC_67_QBmqKY@b5gehAGV#kRu(DYdsf`!F&c9Um>Pu(J|FrzO4a1S|smI6dyiO`5?JGz`XY@@CcM6##{qxLuQn~Oe; zWp?63s7=RD{8exjW_n_;S({UsNL8G>dh;J=NMNhB_$69B4k4nVIc!K67?mYRC+Z-S z;W6D5XL{=u_sVO7&e`Z!(V|;Ctjtr>;durZMED+5#t$r1L8HdZ1;8II+SGF%PAhkn zhSn2bW0eVLm^rkd5FHP*{X%sd>3Josjg0V1Jtp6#J{(wNCfIL)?=j5_BAEB|U>hla zV~FZBb1*9kQgE{&jA2jPhM?i@l<7DOys8&WR=muF-?D>ej6!@mN^`0j5AnBn@;^zR z>_?3646gN(i!#1m5*m$o5;wGeR3B!ZReDy0%5t;vOb%1Fj(%N#2VAe7lbBxGg3T2= z)VTnDvt`<>B}FudeG@9{{Mqzf2k}Ixv=M%oalfBDXaXe^b*P9aB?}R6pmH=Bzop{i z?%vxCIdEK{C6*x`vZMQ0_@+jN2{czr&sThA2gK$DvnW7-T7Ut~JBvq4MdBIE>0cmK z3D_V{FeOq(2Oiv}@*XU10`~6w9Yd_h5p((wlLTFenL|F>2G^S_DGmELJ%0;dRTAZT zIaQV7oYD$&@3p0F8EpbL2w3>7NFak>qTQbe-tTUTH`qK=MAu-p2&1g#M=wFB6KLU2 z#mlFdgM8zAZdM#Es*IH3$Dr^g-jLvuShlIDAa_!2nf&=tM#b2Tp8Xp7zc z<+4#zCYlDvbaOMN+{RYMn4{m0$uJ*2yL)Fd2&91g(PB@Ocd^HY8+}I+{nq8s^bKHQ z=^LJ+XIcOgBv+b7N$u}-Mxh3;o$85#q1-m6xB~oAKJGTnEc}e#P0Vzg?;yHV5 zSzluVl+C3V?{}Tim3l|pp_)2eZ938{8s>k*TJyd=R?YD+-_N7WqPVP<{nVz;Whu;w zR@ikjl$wpT<}{{0v9n1ruzXm;ed_6-%~)uCV&R9FJxQd;BNtX4cuk`^n8E{k{Smit zSFDm+9gb?*X?^a~h?Tw9P+I2{!N;*)-L9q=BG`-R!~8#LUom$3+FQCJ)ek#ZY5WU` zVIL{3IBM%SwjV~HSCf;iEb(!1XwzYVd@AC_9w+{EFIn3mMS5re9{^&x%XY9@Xb8st_yx`hg-gB|M+pe>|?lC}oF{=CV$%GW)nD%@UR3lw=> z&AZaY#+G>z0)8?@evWo}Zbs^83Q;ms!~|;KUck;dJ=-VXUqSh0G7r&sF`oq|)(&NE zljw#=_l@^ba}6trw&1z(GDnSj@HuFhyu#I}gwtnT?NhD&ov*vOS456f zRF~T!YQFYV@tM9fWtnS~MW}YM+(W{a=0~5+dVI)Aaubqw0WplF9deuIjTbcB{C7wM zXFeU80VT{>3Y83BlAjK2e6*;my$`}U2*$NwMD#yBwcv*igc9L&k~dlvWGikBb70e%7D%KpoJ=eSv1{~mDHceWI8 zEz9n3KPFABE;Wz|`sM@#uF>l#0pzKTRXFS^Akx-R`5zM1#$p`K6A+#2i1CuCHrC;o z9DxvE+ZWe15_6Y6K)+9UXsmc#XBI>+B6*ZPVvwa$nrF##^8We=2Nj!{w@QTaI_D zhO=P@M7uG!B5+Nf?Yp%Cywg8tM+b;W6CMGc{hhT6yb~N}nE*t;HIKsD0gPJ%-hq@e e3j(2Qv%#F`WGl!XbwMC8_7BJIAY1J?SN{OKnIJC! literal 0 HcmV?d00001