diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index a448c95e..3b0635cb 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -11,7 +11,6 @@ paths: - pnnl.goss.core/src - pnnl.goss.core.runner/src - pnnl.goss.core.testutil/src - - pnnl.goss.core.itests/src # Paths to ignore paths-ignore: @@ -22,14 +21,3 @@ paths-ignore: - "**/cache/**" - "**/releaserepo/**" - "**/test/**/*.java" # Focus on main source code - -# Disable queries that may produce too many false positives -disable-default-queries: false - -# Additional packs for enhanced security analysis -packs: - - codeql/java-queries:AlertSuppression.ql - - codeql/java-queries:Security/CWE - - codeql/java-queries:Security/CWE/CWE-078.ql # OS Command Injection - - codeql/java-queries:Security/CWE/CWE-089.ql # SQL Injection - - codeql/java-queries:Security/CWE/CWE-798.ql # Hard-coded credentials \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b52f7bfa..add14869 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,26 +21,26 @@ jobs: strategy: fail-fast: false matrix: - java-version: [21, 22] + java-version: [21] include: - java-version: 21 primary: true steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # Full history for better analysis - name: Set up JDK ${{ matrix.java-version }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java-version }} distribution: 'temurin' cache: gradle - name: Cache Gradle dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -54,45 +54,40 @@ jobs: run: chmod +x gradlew - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v2 + uses: gradle/actions/wrapper-validation@v5 - name: Run unit tests run: ./gradlew test --continue --no-daemon env: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - - - name: Run integration tests (non-OSGi) - run: ./gradlew check -x :pnnl.goss.core.itests:testOSGi --continue --no-daemon - env: - GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - + - name: Build project - run: ./gradlew build -x :pnnl.goss.core.itests:testOSGi --no-daemon + run: ./gradlew assemble --no-daemon env: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - name: Generate test report - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@v2 if: success() || failure() with: name: Test Results (JDK ${{ matrix.java-version }}) - path: '**/generated/test-results/test/TEST-*.xml' + path: '**/generated/test-reports/test/TEST-*.xml' reporter: java-junit fail-on-error: false - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: test-results-jdk${{ matrix.java-version }} path: | - **/generated/test-results/ + **/generated/test-reports/ **/generated/reports/ retention-days: 30 - name: Upload build artifacts (primary JDK only) if: matrix.primary && (success() || failure()) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-artifacts path: | @@ -100,18 +95,18 @@ jobs: cnf/releaserepo/**/*.jar retention-days: 90 - osgi-integration-tests: - name: OSGi Integration Tests + integration-tests: + name: Integration Tests runs-on: ubuntu-latest needs: test - if: github.event_name != 'schedule' # Skip on scheduled runs + if: github.event_name != 'schedule' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -120,20 +115,64 @@ jobs: - name: Make gradlew executable run: chmod +x gradlew - - name: Run OSGi integration tests - run: ./gradlew :pnnl.goss.core.itests:testOSGi --no-daemon || true + - name: Build SimpleRunner JAR + run: ./gradlew :pnnl.goss.core.runner:createSimpleRunner --no-daemon env: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - - name: Upload OSGi test results + - name: Install pixi + uses: prefix-dev/setup-pixi@v0.9.4 + with: + pixi-version: latest + manifest-path: pnnl.goss.core.itests/pixi.toml + + - name: Start GOSS SimpleRunner + run: | + rm -rf data/goss-broker + nohup java -jar pnnl.goss.core.runner/generated/executable/goss-simple-runner.jar > goss.log 2>&1 & + echo $! > goss.pid + echo "Waiting for STOMP port 61618..." + elapsed=0 + while ! ss -tln | grep -q ":61618 " && [ $elapsed -lt 30 ]; do + sleep 1 + elapsed=$((elapsed + 1)) + printf "." + done + echo "" + if ! ss -tln | grep -q ":61618 "; then + echo "ERROR: GOSS did not start within 30s" + tail -20 goss.log + exit 1 + fi + echo "GOSS is ready (PID $(cat goss.pid))" + + - name: Run Java external server tests + run: ./gradlew :pnnl.goss.core.itests:testExternal --no-daemon + env: + GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false + + - name: Run STOMP token auth tests + working-directory: pnnl.goss.core.itests + run: pixi run test-stomp-token + + - name: Run STOMP + WebSocket token auth tests + working-directory: pnnl.goss.core.itests + run: pixi run test-token-auth + + - name: Stop GOSS if: always() - uses: actions/upload-artifact@v4 + run: | + if [ -f goss.pid ]; then + kill $(cat goss.pid) 2>/dev/null || true + rm -f goss.pid + fi + + - name: Upload GOSS logs + if: always() + uses: actions/upload-artifact@v6 with: - name: osgi-test-results - path: | - pnnl.goss.core.itests/generated/test-results/ - pnnl.goss.core.itests/generated/reports/ - pnnl.goss.core.itests/**/*.log + name: integration-test-logs + path: goss.log retention-days: 30 build-runners: @@ -144,10 +183,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -162,9 +201,7 @@ jobs: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - name: Build OSGi runners using BndRunnerPlugin - run: | - ./gradlew buildRunner.goss-core --no-daemon - ./gradlew buildRunner.goss-core-ssl --no-daemon + run: ./gradlew buildRunner.goss-core --no-daemon env: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false @@ -172,12 +209,11 @@ jobs: run: | ls -lh pnnl.goss.core.runner/generated/runners/ test -f pnnl.goss.core.runner/generated/runners/goss-core-runner.jar - test -f pnnl.goss.core.runner/generated/runners/goss-core-ssl-runner.jar - echo "✅ All runner JARs built successfully" + echo "All runner JARs built successfully" - name: Upload runner artifacts if: success() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: osgi-runners path: pnnl.goss.core.runner/generated/runners/*.jar @@ -186,17 +222,19 @@ jobs: build-status: name: Build Status runs-on: ubuntu-latest - needs: [test, osgi-integration-tests, build-runners] + needs: [test, integration-tests, build-runners] if: always() steps: - name: Check build status run: | echo "Test job status: ${{ needs.test.result }}" - echo "OSGi job status: ${{ needs.osgi-integration-tests.result }}" + echo "Integration tests job status: ${{ needs.integration-tests.result }}" echo "Build runners job status: ${{ needs.build-runners.result }}" - if [[ "${{ needs.test.result }}" == "success" ]] && [[ "${{ needs.build-runners.result }}" == "success" ]]; then + if [[ "${{ needs.test.result }}" == "success" ]] && \ + [[ "${{ needs.integration-tests.result }}" == "success" ]] && \ + [[ "${{ needs.build-runners.result }}" == "success" ]]; then echo "✅ Core build, tests, and runners passed!" exit 0 else diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 6c0eefcf..5d18d66c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -36,7 +36,7 @@ jobs: - name: Upload Checkstyle results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: checkstyle-results path: | @@ -49,10 +49,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -78,10 +78,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -97,7 +97,7 @@ jobs: - name: Upload PMD results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: pmd-results path: | @@ -110,10 +110,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -129,7 +129,7 @@ jobs: - name: Upload dependency check results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dependency-check-results path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bd45e514..c2dd9ca9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,26 +30,26 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' cache: gradle - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml queries: +security-and-quality - name: Cache Gradle dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -64,20 +64,20 @@ jobs: - name: Build for CodeQL analysis run: | - # Build without tests to speed up analysis - ./gradlew build -x test -x check -x :pnnl.goss.core.itests:testOSGi --no-daemon + # Compile only (no tests) for CodeQL to analyze + ./gradlew assemble --no-daemon env: GRADLE_OPTS: -Xmx3g -Dorg.gradle.daemon=false - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" upload: true - name: Upload CodeQL results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: codeql-results path: | diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index f650054a..cc816866 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -13,10 +13,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -30,7 +30,7 @@ jobs: - name: Comment on PR if formatting fails if: failure() && github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0a7eb39..4f0a0e2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: 'temurin' @@ -41,7 +41,7 @@ jobs: run: chmod +x gradlew - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v2 + uses: gradle/actions/wrapper-validation@v5 - name: Run tests run: ./gradlew test --no-daemon @@ -49,7 +49,7 @@ jobs: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false - name: Build release artifacts - run: ./gradlew build export -x :pnnl.goss.core.itests:testOSGi --no-daemon + run: ./gradlew assemble export --no-daemon env: GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false @@ -82,8 +82,7 @@ jobs: cnf/releaserepo/ \ */generated/*.jar \ README.md \ - LICENSE \ - CLAUDE.md + pnnl.goss.core/LICENSE - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -101,7 +100,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: release-artifacts-${{ github.ref_name }} path: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 224dbc8d..e16bf1b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,55 +7,48 @@ on: branches: [ main, master, develop ] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v4 - + - uses: actions/checkout@v6 + - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - + cache: 'gradle' + - name: Grant execute permission for gradlew run: chmod +x gradlew - - - name: Build with Gradle - run: ./gradlew clean build -x check - + + - name: Build project + run: ./gradlew assemble --no-daemon + env: + GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false + - name: Run unit tests + run: ./gradlew test --continue --no-daemon + env: + GRADLE_OPTS: -Xmx2g -Dorg.gradle.daemon=false + + - name: Verify JARs built run: | - echo "Running GOSS Core Tests..." - ./gradlew :pnnl.goss.core.itests:jar - - # Create a simple test to verify build works - java -version - - # Verify jars were built - ls -la pnnl.goss.core/generated/*.jar || true - ls -la pnnl.goss.core.itests/generated/*.jar || true - - echo "✅ Build and jar creation successful" - - - name: Test Core Functionality - run: | - # Run a simple validation that classes can be loaded - echo "Testing class loading..." - - # Check that core classes exist in the jar - jar tf pnnl.goss.core/generated/pnnl.goss.core.core-api.jar | grep -q "pnnl/goss/core/Client.class" && echo "✅ Client class found" || echo "❌ Client class not found" - jar tf pnnl.goss.core/generated/pnnl.goss.core.goss-client.jar | grep -q "pnnl/goss/core/client/GossClient.class" && echo "✅ GossClient class found" || echo "❌ GossClient class not found" - - echo "✅ Core functionality tests passed" - + ls -la pnnl.goss.core/generated/*.jar + echo "Build and tests successful" + - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: test-results path: | - **/build/reports/ - **/generated/*.jar \ No newline at end of file + **/generated/test-reports/ + **/generated/reports/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 7238112c..18b22d50 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ generated/ target/ out/ bin/ +bin_test/ *.jar !cnf/releaserepo/**/*.jar *.war @@ -143,6 +144,32 @@ coverage/ ## Dependency Check ## dependency-check-data/ +## Python ## +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +*.egg +.eggs/ +dist/ +*.whl +.venv/ +venv/ +env/ +.pytest_cache/ +.mypy_cache/ +htmlcov/ + +## Pixi (Python package manager) ## +pixi.lock +.pixi/ + +## Environment Files ## +.env +.env.* +!.env.example + ## Custom Local Files ## *.local local_* diff --git a/Makefile b/Makefile index 462104e6..642c67fe 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ # GOSS Makefile # Provides version management and release automation -.PHONY: help version release snapshot build test clean push-snapshot-local push-release-local \ - bump-patch bump-minor bump-major next-snapshot check-api format format-check +.PHONY: help version release snapshot build test itest itest-java clean push-snapshot-local push-release-local \ + bump-patch bump-minor bump-major next-snapshot check-api format format-check \ + run run-ssl stop status log # Default target help: @@ -38,6 +39,17 @@ help: @echo " 5. git tag v11.0.0 && git push # Tag and push" @echo " 6. make next-snapshot # Bump to next snapshot" @echo "" + @echo "Integration testing:" + @echo " make itest Build, start GOSS, run Java + Python integration tests, stop GOSS" + @echo " make itest-java Build, start GOSS, run Java external server tests, stop GOSS" + @echo "" + @echo "Running:" + @echo " make run Build and run GOSS in the background (logs to log/goss.log)" + @echo " make run-ssl Build and run GOSS with SSL in the background" + @echo " make stop Stop the background GOSS process" + @echo " make status Check if GOSS is running" + @echo " make log Tail the GOSS log file" + @echo "" @echo "Examples:" @echo " make version" @echo " make release VERSION=11.0.0" @@ -62,13 +74,13 @@ ifndef VERSION endif @python3 scripts/version.py snapshot $(VERSION) -# Build all bundles +# Build all bundles (compile + package, no tests) build: - ./gradlew build + ./gradlew assemble -# Run tests +# Run tests (excludes OSGi integration tests which require a running framework; use 'make itest' for those) test: - ./gradlew check + ./gradlew test --continue # Clean build artifacts clean: @@ -110,3 +122,142 @@ format-check: @echo "Checking code formatting..." ./gradlew spotlessCheck @echo "Format check complete." + +# --- Integration test targets --- + +ITESTS_DIR = pnnl.goss.core.itests +STOMP_PORT = 61618 +GOSS_READY_TIMEOUT = 30 + +# Helper: start GOSS and wait for STOMP port +define start-goss + @mkdir -p $(LOG_DIR) + @if [ -f $(PID_FILE) ] && kill -0 $$(cat $(PID_FILE)) 2>/dev/null; then \ + echo "Stopping existing GOSS (PID $$(cat $(PID_FILE)))..."; \ + kill $$(cat $(PID_FILE)); \ + rm -f $(PID_FILE); \ + sleep 2; \ + fi + @if [ -d $(DATA_DIR) ]; then \ + echo "Cleaning stale KahaDB data..."; \ + rm -rf $(DATA_DIR); \ + fi + @echo "Starting GOSS for integration tests (logging to $(LOG_FILE))..." + @nohup java -jar $(SIMPLE_JAR) >> $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) + @echo "Waiting for GOSS STOMP port $(STOMP_PORT)..." + @elapsed=0; \ + while ! ss -tln 2>/dev/null | grep -q ":$(STOMP_PORT) " && [ $$elapsed -lt $(GOSS_READY_TIMEOUT) ]; do \ + sleep 1; \ + elapsed=$$((elapsed + 1)); \ + printf "."; \ + done; \ + echo ""; \ + if ! ss -tln 2>/dev/null | grep -q ":$(STOMP_PORT) "; then \ + echo "ERROR: GOSS did not start within $(GOSS_READY_TIMEOUT)s"; \ + echo "Last log lines:"; \ + tail -20 $(LOG_FILE); \ + kill $$(cat $(PID_FILE)) 2>/dev/null; rm -f $(PID_FILE); \ + exit 1; \ + fi + @echo "GOSS is ready (PID $$(cat $(PID_FILE)))" +endef + +# Build GOSS, start it, run Java external server tests, then stop GOSS +itest-java: $(SIMPLE_JAR) + $(start-goss) + @echo "" + @echo "Running Java external server tests..." + @./gradlew :pnnl.goss.core.itests:testExternal --no-daemon; rc=$$?; \ + echo ""; \ + echo "Stopping GOSS..."; \ + kill $$(cat $(PID_FILE)) 2>/dev/null; rm -f $(PID_FILE); \ + exit $$rc + +# Build GOSS, start it, run Java + Python integration tests, then stop GOSS +itest: $(SIMPLE_JAR) + $(start-goss) + @echo "" + @echo "Running Java external server tests..." + @./gradlew :pnnl.goss.core.itests:testExternal --no-daemon; java_rc=$$?; \ + echo ""; \ + echo "Running Python STOMP integration tests..."; \ + cd $(ITESTS_DIR) && pixi run test-stomp-token; py_stomp_rc=$$?; cd ..; \ + echo ""; \ + echo "Running Python STOMP + WebSocket token auth tests..."; \ + cd $(ITESTS_DIR) && pixi run test-token-auth; py_token_rc=$$?; cd ..; \ + echo ""; \ + echo "Stopping GOSS..."; \ + kill $$(cat $(PID_FILE)) 2>/dev/null; rm -f $(PID_FILE); \ + if [ $$java_rc -ne 0 ]; then exit $$java_rc; fi; \ + if [ $$py_stomp_rc -ne 0 ]; then exit $$py_stomp_rc; fi; \ + exit $$py_token_rc + +# --- Runtime targets --- + +RUNNER_DIR = pnnl.goss.core.runner +SIMPLE_JAR = $(RUNNER_DIR)/generated/executable/goss-simple-runner.jar +SSL_JAR = $(RUNNER_DIR)/generated/executable/goss-ssl-runner.jar +LOG_DIR = log +LOG_FILE = $(LOG_DIR)/goss.log +PID_FILE = $(LOG_DIR)/goss.pid +DATA_DIR = data/goss-broker + +# Build (if needed) and run GOSS in the background +run: $(SIMPLE_JAR) + @mkdir -p $(LOG_DIR) + @if [ -f $(PID_FILE) ] && kill -0 $$(cat $(PID_FILE)) 2>/dev/null; then \ + echo "GOSS is already running (PID $$(cat $(PID_FILE))). Use 'make stop' first."; \ + exit 1; \ + fi + @rm -f $(DATA_DIR)/KahaDB/lock + @echo "Starting GOSS (logging to $(LOG_FILE))..." + @nohup java -jar $(SIMPLE_JAR) >> $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) + @echo "GOSS started (PID $$(cat $(PID_FILE)))" + +# Build (if needed) and run GOSS with SSL in the background +run-ssl: $(SSL_JAR) + @mkdir -p $(LOG_DIR) + @if [ -f $(PID_FILE) ] && kill -0 $$(cat $(PID_FILE)) 2>/dev/null; then \ + echo "GOSS is already running (PID $$(cat $(PID_FILE))). Use 'make stop' first."; \ + exit 1; \ + fi + @rm -f $(DATA_DIR)/KahaDB/lock + @echo "Starting GOSS with SSL (logging to $(LOG_FILE))..." + @nohup java -jar $(SSL_JAR) >> $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) + @echo "GOSS started with SSL (PID $$(cat $(PID_FILE)))" + +# Build the runner JARs if they don't exist +$(SIMPLE_JAR): + ./gradlew :pnnl.goss.core.runner:createSimpleRunner + +$(SSL_JAR): + ./gradlew :pnnl.goss.core.runner:createSSLRunner + +# Stop the background GOSS process +stop: + @if [ -f $(PID_FILE) ] && kill -0 $$(cat $(PID_FILE)) 2>/dev/null; then \ + echo "Stopping GOSS (PID $$(cat $(PID_FILE)))..."; \ + kill $$(cat $(PID_FILE)); \ + rm -f $(PID_FILE); \ + echo "GOSS stopped."; \ + else \ + echo "GOSS is not running."; \ + rm -f $(PID_FILE); \ + fi + +# Check if GOSS is running +status: + @if [ -f $(PID_FILE) ] && kill -0 $$(cat $(PID_FILE)) 2>/dev/null; then \ + echo "GOSS is running (PID $$(cat $(PID_FILE)))"; \ + else \ + echo "GOSS is not running."; \ + rm -f $(PID_FILE); \ + fi + +# Tail the GOSS log +log: + @if [ -f $(LOG_FILE) ]; then \ + tail -f $(LOG_FILE); \ + else \ + echo "No log file found at $(LOG_FILE)"; \ + fi diff --git a/README.md b/README.md index fbbb450d..dee87878 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # GridOPTICS Software System (GOSS) -[![Build Status](https://github.com/GridOPTICS/GOSS/actions/workflows/build.yml/badge.svg)](https://github.com/GridOPTICS/GOSS/actions) +[![CI](https://github.com/GridOPTICS/GOSS/actions/workflows/ci.yml/badge.svg)](https://github.com/GridOPTICS/GOSS/actions/workflows/ci.yml) +[![Tests](https://github.com/GridOPTICS/GOSS/actions/workflows/test.yml/badge.svg)](https://github.com/GridOPTICS/GOSS/actions/workflows/test.yml) +[![CodeQL](https://github.com/GridOPTICS/GOSS/actions/workflows/codeql.yml/badge.svg)](https://github.com/GridOPTICS/GOSS/actions/workflows/codeql.yml) +[![Java 21](https://img.shields.io/badge/Java-21-blue)](https://openjdk.org/projects/jdk/21/) +[![License](https://img.shields.io/badge/License-BSD--2--Clause-green)](pnnl.goss.core/LICENSE) GOSS is a JMS-based messaging framework providing client/server architecture, request/response patterns, and security integration for distributed power grid applications. It serves as the foundation for [GridAPPS-D](https://github.com/GRIDAPPSD/GOSS-GridAPPS-D) and other grid simulation platforms. @@ -48,12 +52,6 @@ java -jar pnnl.goss.core.runner/generated/runners/goss-core-runner.jar - **Use case**: Production, modular deployments - **Includes**: Updated dependencies (ActiveMQ 6.2.0, Jakarta JMS, Shiro 2.0) -#### Option C: SSL-Enabled OSGi Runner -```bash -./gradlew buildRunner.goss-core-ssl -java -jar pnnl.goss.core.runner/generated/runners/goss-core-ssl-runner.jar -``` - **3. Verify GOSS is running:** Once started, you can use these Gogo shell commands: @@ -76,7 +74,6 @@ GOSS includes a **BndRunnerPlugin** that creates executable OSGi JARs from any ` **Examples:** ```bash ./gradlew buildRunner.goss-core # Uses goss-core.bndrun -./gradlew buildRunner.goss-core-ssl # Uses goss-core-ssl.bndrun ./gradlew buildRunner.my-app # Uses my-app.bndrun ``` @@ -273,28 +270,16 @@ I'll wait here # Message was stored and delivered when consumer connected! ``` -### JWT Token Authentication Support -GOSS now includes optional JWT (JSON Web Token) authentication support: +### Security and Authentication -```java -// Create client with token authentication -ClientFactory factory = // ... get factory -Client client = factory.create(PROTOCOL.OPENWIRE, credentials, true); // useToken=true -``` +GOSS uses [Apache Shiro](https://shiro.apache.org/) integrated with ActiveMQ via the ShiroPlugin. All broker connections (OpenWire, STOMP) require authentication and are subject to permission checks. -**New Security Classes:** -- `JWTAuthenticationToken` - Token data structure and parsing -- `SecurityConfig` - Token validation and creation interface -- `GossSecurityManager` - Enhanced security management -- `RoleManager` - Role-based permission management +See the **[Security Guide](docs/SECURITY.md)** for full details on user management, permissions, and token authentication. -**Security Configuration:** -```properties -goss.system.use.token=true -goss.system.token.secret=your-secret-key -goss.system.manager=admin -goss.system.manager.password=admin-password -``` +**Quick overview:** +- Users are configured in `pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg` +- Permissions control access to queues, topics, and temporary queues +- JWT tokens allow clients to authenticate once and reconnect without re-sending credentials ### Session Auto-Renewal Clients now automatically renew their JMS session when publish operations fail, improving reliability in long-running applications. @@ -316,6 +301,11 @@ make version # Show versions of all bundles make build # Build all bundles make test # Run tests make clean # Clean build artifacts +make run # Build and run GOSS in the background (logs to log/goss.log) +make stop # Stop the background GOSS process +make status # Check if GOSS is running +make log # Tail the GOSS log file +make itest # Build, start GOSS, run integration tests, stop GOSS ``` #### API Change Detection @@ -428,6 +418,9 @@ When making changes to GOSS, follow these guidelines: - **[Developer Setup](docs/DEVELOPER-SETUP.md)** - Complete development environment setup for Eclipse and VS Code - **[Production Deployment](docs/PRODUCTION-DEPLOYMENT.md)** - Production deployment guide with systemd, SSL, and monitoring +### Security +- **[Security Guide](docs/SECURITY.md)** - Authentication, permissions, JWT tokens, and user management + ### Development - **[Code Formatting Guide](docs/FORMATTING.md)** - Code style and formatting configuration for consistent code across IDEs @@ -593,7 +586,7 @@ GOSS serves as the messaging foundation for: ## License -This project is licensed under the BSD-3-Clause License. See [LICENSE](LICENSE) for details. +This project is licensed under the BSD-2-Clause License. See [LICENSE](pnnl.goss.core/LICENSE) for details. ## Contributing diff --git a/cnf/ext/central.maven b/cnf/ext/central.maven index dcbc0711..c4b91af8 100644 --- a/cnf/ext/central.maven +++ b/cnf/ext/central.maven @@ -116,6 +116,11 @@ biz.aQute.bnd:biz.aQute.launcher:6.4.0 biz.aQute.bnd:biz.aQute.junit:6.4.0 biz.aQute.bnd:biz.aQute.tester.junit-platform:6.4.0 +# Nimbus JOSE+JWT (for JWT token support) +com.nimbusds:nimbus-jose-jwt:9.31 +net.minidev:json-smart:2.4.11 +net.minidev:accessors-smart:2.4.9 + # Additional dependencies com.thoughtworks.xstream:xstream:1.4.20 commons-dbcp:commons-dbcp:1.4 diff --git a/cnf/ext/libraries.bnd b/cnf/ext/libraries.bnd index fbb528bf..d68a33dd 100644 --- a/cnf/ext/libraries.bnd +++ b/cnf/ext/libraries.bnd @@ -288,3 +288,11 @@ h2-runpath: ${h2};version=file commons-pool2: ${repo;org.apache.commons:commons-pool2;[2.11.0,3);HIGHEST} commons-pool2-buildpath: ${commons-pool2};version=file commons-pool2-runpath: ${commons-pool2};version=file + +# Nimbus JOSE+JWT (for JWT token support) +nimbus-jose-jwt: ${repo;com.nimbusds:nimbus-jose-jwt;[9.31,10);HIGHEST} +json-smart: ${repo;net.minidev:json-smart;[2.4.0,3);HIGHEST} +nimbus-jose-jwt-buildpath: ${nimbus-jose-jwt};version=file,\ + ${json-smart};version=file +nimbus-jose-jwt-runpath: ${nimbus-jose-jwt};version=file,\ + ${json-smart};version=file diff --git a/cnf/releaserepo/index.xml b/cnf/releaserepo/index.xml index 10755260..091e0044 100644 --- a/cnf/releaserepo/index.xml +++ b/cnf/releaserepo/index.xml @@ -1,60 +1,49 @@ - + - + - + + + + + + + + + - - + + - - + + - + - - + + - - - - - - - - - - - - - - + + - - + + - - + + - - - - - - - - + @@ -63,165 +52,96 @@ - + - + - - + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - + + + + + - + - + - + - - - - - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - + @@ -230,36 +150,24 @@ - - - - - - - - - + - + - + - + - - - - - - + + - + @@ -267,218 +175,248 @@ - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - - - - - - - - + - + - + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + - - + + + - - - + + - + - + - - - - - - + + + + + + - - + + - - + + - + - - - - - - - - - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + @@ -489,19 +427,19 @@ - + - + - + - + @@ -509,315 +447,387 @@ - + + + + + + + + + - + - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + - + - - - - - - - - - + - - + + - - + + - + - - + + - - - - - - + + - + - + - + - + - - + + - - + + - + - - - - - - + + + + + + - - + + - - + + - - + + - - - - - - + + - - + + - - + + - - + + - + - - + + - - - - - - - - + - + - + - - - - - + + + + + + - - + + - - + + - + - - + + - - + + - - + + + + + + - + - + - + - - + + - - + + - + - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - - + + + - - + + + - - + + + + + + + + + + @@ -826,28 +836,28 @@ - + - + - + - + - + - - + + @@ -884,67 +894,73 @@ - + - + + + + + + + + + - - + + - - + + - + - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -955,23 +971,23 @@ - + - + - + - + - + @@ -979,140 +995,97 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + - - + + - - + + - + - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + @@ -1120,188 +1093,87499 @@ - - - - - - - - - - - - - - - - - - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - + + + - - - - - + + - + - + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + - - + + + - - - + + + + + + + + + + + + + + - + - - - - - - - - - + - - + + - - + + - + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + - + - + - + + - - - + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + @@ -1310,41 +88594,260 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - + - + - + - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1352,9 +88855,75 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1364,206 +88933,361 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + - - + + - - + + - + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + + - - + + - + - - - - - - + + - - - - - - - - - - - - - + - + - + - - + + - + - + - + - - - - - - - - + - + - - - - - - - - - + - - + + - - + + - + - - - - - - + + - + @@ -1571,552 +89295,366 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - + + + + + + + + + - + - + - - - - - - - - - + - - + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - - - - - - - - - + - + - - - - - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - + - - + + - - + + - + - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - - - - - - - - - + - - + + - - + + - + - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - - - + - + - + - - - - - - + + + + + + - - + + - - + + - + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + @@ -2127,314 +89665,211 @@ - + - + - + - + + + + + + + + + - + - + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - + - - + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - + - - - - - - - - - + - + - - - - - + - - - - - + - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - + - - + + - - + + - + - - + + - - + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + - + @@ -2443,141 +89878,106 @@ - + - + - + - + - + - + - - + + - - - - - - - - - - - - - - - - - + - + - + - + - + - - + + - - + + - + - - - - - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + @@ -2586,221 +89986,130 @@ - + - + - + - + - - - - - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + - + - + - - + + - - + + - + - - + + - - - - - - - - + + - - - + + - - - + + - - - + + - - - + + - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + @@ -2809,21 +90118,21 @@ - + - + - + - + - - + + @@ -2892,331 +90201,508 @@ - + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + - + - + - + - - + + - - + + - + - - + + - - - + + - - + + + + + + + + + + + + + + + + + + + + + - - + - - - + + - - - + + - - - + + - - - + + - + + + + + + + + + + + + + + + + + - + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + - + - + - - - - - - - - + - - + + - - + + - + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - - + + - - + + - + - - + + - - - + + - - - + + - - - + + - - - + + - - - + + - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + @@ -3231,243 +90717,361 @@ - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - - + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - + - - - - - - - - - - - - - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - - - - - - + + + + + + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + @@ -3476,44 +91080,24 @@ - - - - - - - - - + - + - + - + - - - - - - - - - - + + - - - - - + @@ -3521,52 +91105,12 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -3577,19 +91121,19 @@ - + - + - + - + @@ -3597,365 +91141,315 @@ - + + + + + + + + + - + - + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - - + + - - + + - + - - - - - - + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + - - + + + - - - + + - + - + - - - - - + + + + + + - - + + - - + + - + - - - - - - + + - - - - - - - - - - + + - - + + - - - - + - + - + - - + + - - + + - + - - - - - - + + - - - - - - - - - - - - - + - + - + - - + + - + - + - + - - - - - - - - + - + - + - - + + - - + + - + - - - - - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + diff --git a/cnf/releaserepo/index.xml.sha b/cnf/releaserepo/index.xml.sha index 2b1e1281..3b125637 100644 --- a/cnf/releaserepo/index.xml.sha +++ b/cnf/releaserepo/index.xml.sha @@ -1 +1 @@ -d9805919b8a7bf9193720ac4c556ea0c6af88e6db1fd2e9911b6971aace17ed2 \ No newline at end of file +e0af14eda55e4617ea5d893de3632410eb36d9f2dd1085748f166d5bd1721fe5 \ No newline at end of file diff --git a/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-13.0.0.jar new file mode 100644 index 00000000..e3b97953 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-14.0.0.jar new file mode 100644 index 00000000..7560df26 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.0.jar new file mode 100644 index 00000000..c7264f50 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.1.jar new file mode 100644 index 00000000..66f7e215 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.2.jar new file mode 100644 index 00000000..0b7886b1 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.core-api/pnnl.goss.core.core-api-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-13.0.0.jar new file mode 100644 index 00000000..efb9f5ae Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-14.0.0.jar new file mode 100644 index 00000000..8a3a6495 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.0.jar new file mode 100644 index 00000000..f7dd92c8 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.1.jar new file mode 100644 index 00000000..7cf8f70a Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.2.jar new file mode 100644 index 00000000..b3f27f5a Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-client/pnnl.goss.core.goss-client-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-13.0.0.jar new file mode 100644 index 00000000..86328044 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-14.0.0.jar new file mode 100644 index 00000000..f4ba048f Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.0.jar new file mode 100644 index 00000000..c9bf96f2 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.1.jar new file mode 100644 index 00000000..5072d889 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.2.jar new file mode 100644 index 00000000..5702576c Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-commands/pnnl.goss.core.goss-core-commands-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-13.0.0.jar new file mode 100644 index 00000000..3964dfa3 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-14.0.0.jar new file mode 100644 index 00000000..e7f2d8aa Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.0.jar new file mode 100644 index 00000000..eaba68eb Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.1.jar new file mode 100644 index 00000000..553234d5 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.2.jar new file mode 100644 index 00000000..d38bffb8 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-exceptions/pnnl.goss.core.goss-core-exceptions-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-13.0.0.jar new file mode 100644 index 00000000..587973df Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-14.0.0.jar new file mode 100644 index 00000000..4dbfc082 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.0.jar new file mode 100644 index 00000000..b625272c Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.1.jar new file mode 100644 index 00000000..c8b89439 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.2.jar new file mode 100644 index 00000000..cbca396d Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-security/pnnl.goss.core.goss-core-security-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-13.0.0.jar new file mode 100644 index 00000000..73706a91 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-14.0.0.jar new file mode 100644 index 00000000..3e5714a5 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.0.jar new file mode 100644 index 00000000..da356b44 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.1.jar new file mode 100644 index 00000000..f00fb1aa Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.2.jar new file mode 100644 index 00000000..8801f013 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-api/pnnl.goss.core.goss-core-server-api-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-13.0.0.jar new file mode 100644 index 00000000..f5d829ce Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-14.0.0.jar new file mode 100644 index 00000000..be019247 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.0.jar new file mode 100644 index 00000000..18370cca Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.1.jar new file mode 100644 index 00000000..3d5c6927 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.2.jar new file mode 100644 index 00000000..32481d18 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-registry/pnnl.goss.core.goss-core-server-registry-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-13.0.0.jar new file mode 100644 index 00000000..4ffe6075 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-14.0.0.jar new file mode 100644 index 00000000..c0eb6b84 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.0.jar new file mode 100644 index 00000000..4a1b7c5b Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.1.jar new file mode 100644 index 00000000..e18b8625 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.2.jar new file mode 100644 index 00000000..b44acc35 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server-web/pnnl.goss.core.goss-core-server-web-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-13.0.0.jar new file mode 100644 index 00000000..1a927b26 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-14.0.0.jar new file mode 100644 index 00000000..93a1c0bc Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.0.jar new file mode 100644 index 00000000..adf42a2b Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.1.jar new file mode 100644 index 00000000..87e6e7e2 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.2.jar new file mode 100644 index 00000000..70de11ef Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.goss-core-server/pnnl.goss.core.goss-core-server-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-13.0.0.jar new file mode 100644 index 00000000..9be970f8 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-14.0.0.jar new file mode 100644 index 00000000..2351d999 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.0.jar new file mode 100644 index 00000000..26058b84 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.1.jar new file mode 100644 index 00000000..e1de389f Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.2.jar new file mode 100644 index 00000000..82bfbb59 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.itests/pnnl.goss.core.itests-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-13.0.0.jar new file mode 100644 index 00000000..a91a65c6 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-14.0.0.jar new file mode 100644 index 00000000..d82e2282 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.0.jar new file mode 100644 index 00000000..43eceb12 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.1.jar new file mode 100644 index 00000000..7cf8a95a Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.2.jar new file mode 100644 index 00000000..ce8eef13 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.runner/pnnl.goss.core.runner-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-13.0.0.jar new file mode 100644 index 00000000..3ec88205 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-14.0.0.jar new file mode 100644 index 00000000..9e43d943 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.0.jar new file mode 100644 index 00000000..07440f86 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.1.jar new file mode 100644 index 00000000..df026bd7 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.2.jar new file mode 100644 index 00000000..87c5e788 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-jwt/pnnl.goss.core.security-jwt-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-13.0.0.jar new file mode 100644 index 00000000..1ac5b233 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-14.0.0.jar new file mode 100644 index 00000000..19636383 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.0.jar new file mode 100644 index 00000000..a03485c2 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.1.jar new file mode 100644 index 00000000..6e10d904 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.2.jar new file mode 100644 index 00000000..a2e29214 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-ldap/pnnl.goss.core.security-ldap-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-13.0.0.jar new file mode 100644 index 00000000..7c83c722 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-14.0.0.jar new file mode 100644 index 00000000..2ea3c9ab Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.0.jar new file mode 100644 index 00000000..a4b203e0 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.1.jar new file mode 100644 index 00000000..4e5369ff Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.2.jar new file mode 100644 index 00000000..73130e97 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-propertyfile/pnnl.goss.core.security-propertyfile-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-13.0.0.jar new file mode 100644 index 00000000..e5dc9d85 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-14.0.0.jar new file mode 100644 index 00000000..8f5d0e74 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.0.jar new file mode 100644 index 00000000..f466adf5 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.1.jar new file mode 100644 index 00000000..63fb5e90 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.2.jar new file mode 100644 index 00000000..a50e4895 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.security-system/pnnl.goss.core.security-system-15.0.2.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-13.0.0.jar b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-13.0.0.jar new file mode 100644 index 00000000..d7839cf4 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-13.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-14.0.0.jar b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-14.0.0.jar new file mode 100644 index 00000000..c6e72a20 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-14.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.0.jar b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.0.jar new file mode 100644 index 00000000..86d083c2 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.0.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.1.jar b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.1.jar new file mode 100644 index 00000000..8fe69b69 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.1.jar differ diff --git a/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.2.jar b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.2.jar new file mode 100644 index 00000000..e21cd1c3 Binary files /dev/null and b/cnf/releaserepo/pnnl.goss.core.testutil/pnnl.goss.core.testutil-15.0.2.jar differ diff --git a/docs/README.md b/docs/README.md index 4ceacb93..0eed455b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,20 @@ Complete development environment setup for both Eclipse and VS Code IDEs. - Debugging GOSS applications - OSGi bundle development +## Security + +### [Security Guide](SECURITY.md) + +Authentication, permissions, JWT tokens, and user management. + +**Topics:** + +- User management via property files +- Permission format and wildcards +- JWT token authentication flow +- STOMP client token workflow +- Security architecture + ## Development Guides ### [Code Formatting Guide](FORMATTING.md) @@ -112,11 +126,9 @@ Production deployment guide with systemd, SSL, and monitoring. # Create executable JARs (OSGi runners with updated dependencies) ./gradlew buildRunner.goss-core -./gradlew buildRunner.goss-core-ssl # Create simple fat JARs ./gradlew :pnnl.goss.core.runner:createSimpleRunner -./gradlew :pnnl.goss.core.runner:createSSLRunner # Check code formatting ./gradlew spotlessCheck @@ -136,8 +148,7 @@ java -jar goss-simple-runner.jar **Option B: OSGi Runner (Production)** ```bash cd pnnl.goss.core.runner/generated/runners -java -jar goss-core-runner.jar # Standard -java -jar goss-core-ssl-runner.jar # With SSL +java -jar goss-core-runner.jar ``` ### GOSS Shell Commands diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 00000000..896b6122 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,285 @@ +# GOSS Security Guide + +GOSS uses [Apache Shiro](https://shiro.apache.org/) for authentication and authorization, integrated with ActiveMQ via the [ShiroPlugin](https://activemq.apache.org/security). All broker connections (OpenWire, STOMP) require valid credentials and are subject to permission checks. + +## User Management + +### Property File Configuration + +Users are defined in a property file with one user per line: + +**File**: `pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg` + +**Format**: +``` +username=password,permission1,permission2,... +``` + +**Example**: +```properties +system=manager,queue:*,topic:*,temp-queue:* +craig=craig,queue:*,topic:*,temp-queue:* +july=july,queue:*,topic:*,temp-queue:* +``` + +Each line defines: +- **username** - The login name (left of `=`) +- **password** - The first value after `=` (plaintext) +- **permissions** - Comma-separated permission strings + +### Adding a New User + +Add a line to the property file and restart GOSS: + +```properties +alice=secretpass,queue:*,topic:*,temp-queue:* +``` + +For a read-only user that can only subscribe to topics: + +```properties +reader=readerpass,topic:* +``` + +### System User + +The `system` user is the administrative account used internally by GOSS for broker management and token handling. It must always be present with at least `queue:*,topic:*,temp-queue:*` permissions. + +## Permissions + +### Permission Format + +Permissions use a colon-separated hierarchical format: + +``` +type:destination[:action] +``` + +| Part | Description | Examples | +|------|-------------|----------| +| **type** | Destination type | `queue`, `topic`, `temp-queue` | +| **destination** | Destination name or wildcard | `*`, `goss.gridappsd.process.request`, `ActiveMQ.Advisory.*` | +| **action** | Optional action | `read`, `write`, `create`, `subscribe` | + +### Wildcards + +The `*` wildcard matches any value at that level: + +| Permission | Grants access to | +|------------|-----------------| +| `queue:*` | All queues, all actions | +| `topic:*` | All topics, all actions | +| `temp-queue:*` | All temporary queues | +| `topic:goss.gridappsd.*` | All topics under `goss.gridappsd` | +| `queue:request:write` | Write-only access to the `request` queue | + +### Common Permission Sets + +**Full access** (admin/system users): +``` +queue:*,topic:*,temp-queue:* +``` + +**GridAPPS-D application** (typical simulation app): +``` +topic:goss.gridappsd.simulation.*,queue:goss.gridappsd.process.request,temp-queue:* +``` + +**Read-only subscriber**: +``` +topic:* +``` + +### How Permissions Are Enforced + +GOSS uses `GossWildcardPermissionResolver` to route permission checks: + +- Permissions starting with `topic:`, `queue:`, or `temp-queue:` are handled by ActiveMQ's `ActiveMQWildcardPermission` and enforced at the broker level +- All other permissions use Shiro's standard `WildcardPermission` for application-level access control + +When a client attempts to send to or subscribe on a destination, ActiveMQ's ShiroPlugin checks the client's permissions against the requested destination. If the permission check fails, the operation is rejected. + +## JWT Token Authentication + +JWT (JSON Web Token) authentication allows clients to authenticate once with credentials and then reconnect using a token, avoiding repeated credential transmission. + +### Token Flow + +``` +Client GOSS Server + | | + | 1. CONNECT (username/password) | + |-------------------------------------------->| + | | + | 2. CONNECTED | + |<--------------------------------------------| + | | + | 3. SUBSCRIBE /queue/temp.reply.xyz | + |-------------------------------------------->| + | | + | 4. SEND to /topic/pnnl.goss.token.topic | + | body: base64(username:password) | + | reply-to: /queue/temp.reply.xyz | + |-------------------------------------------->| + | | + | 5. MESSAGE on /queue/temp.reply.xyz | + | body: | + |<--------------------------------------------| + | | + | 6. DISCONNECT | + |-------------------------------------------->| + | | + | 7. CONNECT (token as username, empty pass) | + |-------------------------------------------->| + | | + | 8. CONNECTED (authenticated via token) | + |<--------------------------------------------| +``` + +### Step-by-step + +1. **Connect with credentials** - Standard STOMP/OpenWire connection with username and password +2. **Subscribe to a reply queue** - Create a temporary queue to receive the token response +3. **Request a token** - Send `base64(username:password)` to the token topic (`/topic/pnnl.goss.token.topic`) with a `reply-to` header pointing to your reply queue +4. **Receive the token** - The server validates credentials and responds with a JWT token (or `"authentication failed"`) +5. **Reconnect with token** - Disconnect and reconnect using the JWT token as the username with an empty password + +### Token Structure + +Tokens are signed with HMAC-SHA256 (HS256) and contain: + +| Claim | Description | +|-------|-------------| +| `sub` | Username | +| `roles` | List of permission strings | +| `iat` | Issued-at timestamp | +| `exp` | Expiration (5 days from issuance) | + +### STOMP Client Example (Python) + +```python +import base64 +import stomp +import time + +HOST = "localhost" +PORT = 61618 +USERNAME = "system" +PASSWORD = "manager" +TOKEN_TOPIC = "/topic/pnnl.goss.token.topic" + +# Step 1: Connect with credentials +conn = stomp.Connection12([(HOST, PORT)]) +conn.connect(USERNAME, PASSWORD, wait=True) + +# Step 2: Set up a listener for the token response +class TokenListener(stomp.ConnectionListener): + def __init__(self): + self.token = None + def on_message(self, frame): + self.token = frame.body + +listener = TokenListener() +conn.set_listener("token", listener) + +# Step 3: Subscribe to reply queue and request token +reply_queue = "/queue/temp.token.reply" +conn.subscribe(destination=reply_queue, id="1", ack="auto") +time.sleep(0.3) + +payload = base64.b64encode(f"{USERNAME}:{PASSWORD}".encode()).decode() +conn.send( + destination=TOKEN_TOPIC, + body=payload, + headers={"reply-to": reply_queue}, +) + +# Step 4: Wait for token +time.sleep(2) +token = listener.token +print(f"Got token: {token[:50]}...") +conn.disconnect() + +# Step 5: Reconnect with token (token as username, empty password) +conn2 = stomp.Connection12([(HOST, PORT)]) +conn2.connect(token, "", wait=True) +print("Connected with token!") +conn2.disconnect() +``` + +### Java Client Example + +```java +// Using GOSS ClientFactory with token support +ClientFactory factory = // ... get from OSGi or direct instantiation +Credentials creds = new UsernamePasswordCredentials(username, password); +Client client = factory.create(PROTOCOL.OPENWIRE, creds, true); // useToken=true +``` + +### Security Configuration (OSGi) + +For OSGi deployments, JWT settings are configured via: + +```properties +goss.system.manager=system +goss.system.manager.password=manager +``` + +The JWT signing key is derived automatically from the system manager credentials. For custom key management, implement the `SecurityConfig` interface. + +## Architecture + +### Authentication Realms + +GOSS registers multiple Shiro realms with the security manager: + +| Realm | Purpose | Authentication method | +|-------|---------|----------------------| +| **PropertyRealm** | Property-file users | Username + password | +| **TokenRealm** | JWT token users | Token as username, empty password | +| **SystemRealm** (OSGi) | System admin account | Hardcoded system credentials | + +Token-based authentication is detected by: username length > 250 characters and empty password. This heuristic works because JWT tokens are always longer than any reasonable username. + +### Key Security Classes + +| Class | Package | Role | +|-------|---------|------| +| `SecurityConfigImpl` | `pnnl.goss.core.security.impl` | JWT token creation and validation (HS256) | +| `JWTAuthenticationToken` | `pnnl.goss.core.security` | JWT claims data structure | +| `GossWildcardPermissionResolver` | `pnnl.goss.core.security.impl` | Routes ActiveMQ vs. application permissions | +| `PropertyBasedRealm` | `pnnl.goss.core.security.propertyfile` | Property-file user authentication (OSGi) | +| `GossSecurityManager` | `pnnl.goss.core.security` | Central security coordination | +| `RoleManager` | `pnnl.goss.core.security` | Role-to-permission mapping | + +### SimpleRunner vs. OSGi Security + +The **SimpleRunner** (`GossSimpleRunner`) wires security directly without OSGi: +- Reads users from the property file at startup +- Creates `PropertyRealm` and `TokenRealm` as inner classes +- Handles token requests via a direct JMS listener on the token topic +- Attaches `ShiroPlugin` to the embedded ActiveMQ broker + +The **OSGi Runner** uses Declarative Services to wire security components: +- `PropertyBasedRealm`, `SystemRealm`, `UnauthTokenBasedRealm` are OSGi components +- `UserRepositoryImpl` handles token requests via GOSS Client abstractions +- `RoleManagerImpl` provides role-to-permission mapping from configuration files + +## Integration Testing + +Run the STOMP token authentication integration tests: + +```bash +make itest +``` + +This builds GOSS, starts it in the background, runs the Python test suite against the STOMP port, and stops GOSS when done. The tests verify: + +1. Credential-based STOMP connection +2. JWT token request and response +3. Token-based reconnection +4. Publish/subscribe with token authentication +5. Invalid credential rejection +6. Empty token rejection + +The test suite is in `pnnl.goss.core.itests/` and uses [pixi](https://pixi.sh/) for Python dependency management. diff --git a/pnnl.goss.core.itests/.classpath b/pnnl.goss.core.itests/.classpath index 735afd17..cfc524c1 100644 --- a/pnnl.goss.core.itests/.classpath +++ b/pnnl.goss.core.itests/.classpath @@ -1,12 +1,5 @@ - - - - - - - diff --git a/pnnl.goss.core.itests/.gitattributes b/pnnl.goss.core.itests/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/pnnl.goss.core.itests/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/pnnl.goss.core.itests/.gitignore b/pnnl.goss.core.itests/.gitignore index 90dde36e..61735c10 100644 --- a/pnnl.goss.core.itests/.gitignore +++ b/pnnl.goss.core.itests/.gitignore @@ -1,3 +1 @@ -/bin/ -/bin_test/ -/generated/ +# Covered by root .gitignore diff --git a/pnnl.goss.core.itests/bnd.bnd b/pnnl.goss.core.itests/bnd.bnd index b7989c76..e1c6a871 100644 --- a/pnnl.goss.core.itests/bnd.bnd +++ b/pnnl.goss.core.itests/bnd.bnd @@ -1,4 +1,4 @@ -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 # Build dependencies - JUnit 5 # Note: Using osgi-core-buildpath only (not full osgi-buildpath) to avoid javax.jms from osgi.enterprise @@ -12,7 +12,6 @@ Bundle-Version: 12.1.0 org.junit.platform:junit-platform-launcher;version='[1.10.0,2)',\ org.opentest4j:opentest4j;version='[1.3.0,2)',\ org.apiguardian:apiguardian-api;version='[1.1.0,2)',\ - biz.aQute.tester.junit-platform;version='[6.4.0,7)',\ ${slf4j-buildpath},\ ${activemq-buildpath},\ org.apache.shiro:shiro-core;version=2.0.0,\ @@ -30,16 +29,6 @@ Bundle-Version: 12.1.0 pnnl.goss.core.goss-core-server-registry;version=snapshot,\ pnnl.goss.core.testutil;version=latest -# Use JUnit 5 tester --tester: biz.aQute.tester.junit-platform - -# OSGi Runtime Configuration --runfw: org.apache.felix.framework;version='[7.0.5,8)' --runee: JavaSE-21 - -# Test discovery - JUnit 5 -Test-Cases: ${classes;ANNOTATION;org.junit.jupiter.api.Test} - # Private packages Private-Package: \ pnnl.goss.core.itests,\ @@ -59,54 +48,3 @@ Import-Package: \ org.osgi.framework.*,\ * -# Disable baselining for integration tests -#-baselining: * - -# Modern launcher configuration -# SPI Fly is a framework extension for ServiceLoader support --runpath: \ - biz.aQute.launcher;version='[6.4.0,7)',\ - ${spifly-runpath} - -# Runtime bundles for OSGi tests - JUnit 5 -# Using library macros and skip osgi.service resolution for simpler setup --resolve.effective: active;skip:="osgi.service" - --runbundles: \ - ${spifly-bundle-runpath},\ - ${osgi-util-runpath},\ - ${activemq-runpath},\ - ${jakarta-runpath},\ - ${jakarta-annotation-api-runpath},\ - ${jakarta-xml-bind-api-runpath},\ - ${jakarta-activation-runpath},\ - ${slf4j-runpath},\ - ${configadmin-runpath},\ - ${fileinstall-runpath},\ - ${gson-runpath},\ - ${httpclient-osgi-runpath},\ - ${osgi-service-component-runpath},\ - ${commons-dbcp-runpath},\ - ${commons-pool-runpath},\ - ${commons-logging-runpath},\ - ${commons-beanutils-runpath},\ - ${geronimo-jta-runpath},\ - ${xstream-runpath},\ - ${commons-io-runpath},\ - junit-platform-commons;version='[1.11.0,2)',\ - junit-platform-engine;version='[1.11.0,2)',\ - junit-platform-launcher;version='[1.11.0,2)',\ - junit-jupiter-api;version='[5.11.0,6)',\ - junit-jupiter-engine;version='[5.11.0,6)',\ - org.opentest4j;version='[1.3.0,2)',\ - org.apache.felix.scr;version='[2.2.0,3)',\ - pnnl.goss.core.core-api;version=snapshot,\ - pnnl.goss.core.goss-client;version=snapshot,\ - pnnl.goss.core.goss-core-server;version=snapshot,\ - pnnl.goss.core.goss-core-server-api;version=snapshot,\ - pnnl.goss.core.goss-core-exceptions;version=snapshot,\ - pnnl.goss.core.goss-core-server-registry;version=snapshot,\ - pnnl.goss.core.goss-core-security;version=snapshot,\ - pnnl.goss.core.goss-core-commands;version=snapshot,\ - pnnl.goss.core.security-propertyfile;version=snapshot,\ - pnnl.goss.core.testutil;version=snapshot diff --git a/pnnl.goss.core.itests/build.gradle b/pnnl.goss.core.itests/build.gradle index f0eb0982..fae484cf 100644 --- a/pnnl.goss.core.itests/build.gradle +++ b/pnnl.goss.core.itests/build.gradle @@ -25,9 +25,39 @@ dependencies { test { useJUnitPlatform() - + + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + } +} + +// Tests against an already-running GOSS server (no embedded broker). +// Usage: +// ./gradlew :pnnl.goss.core.itests:testExternal +// ./gradlew :pnnl.goss.core.itests:testExternal -Dgoss.openwire.uri=tcp://myhost:61616 +tasks.register('testExternal', Test) { + useJUnitPlatform() + + description = 'Run integration tests against an external GOSS server' + group = 'verification' + + // BND projects put all sources in src/ (main source set), not src/test/java + testClassesDirs = sourceSets.main.output.classesDirs + classpath = sourceSets.main.runtimeClasspath + + // Only include the external-server test class + include '**/GossExternalServerTest.class' + + // Forward system properties so the test can find the server + systemProperties System.properties.subMap([ + 'goss.openwire.uri', 'goss.stomp.uri', + 'goss.username', 'goss.password' + ]) + testLogging { events "passed", "skipped", "failed" exceptionFormat "full" + showStandardStreams = true } } \ No newline at end of file diff --git a/pnnl.goss.core.itests/itest.bnd b/pnnl.goss.core.itests/itest.bnd index eb0cf728..24c9501d 100644 --- a/pnnl.goss.core.itests/itest.bnd +++ b/pnnl.goss.core.itests/itest.bnd @@ -1,5 +1,5 @@ # Modern OSGi Integration Test Configuration -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 # Use JUnit 5 and OSGi Test # Note: Using osgi-core-buildpath to avoid javax.jms from osgi.enterprise diff --git a/pnnl.goss.core.itests/pixi.toml b/pnnl.goss.core.itests/pixi.toml new file mode 100644 index 00000000..a07a9223 --- /dev/null +++ b/pnnl.goss.core.itests/pixi.toml @@ -0,0 +1,17 @@ +[workspace] +authors = ["Craig <3979063+craig8@users.noreply.github.com>"] +channels = ["conda-forge"] +name = "pnnl.goss.core.itests" +platforms = ["linux-64"] +version = "0.1.0" + +[tasks] +test-stomp-token = "pytest src/pnnl/goss/core/itests/test_stomp_token_auth.py -v" +test-stomp-token-standalone = "python src/pnnl/goss/core/itests/test_stomp_token_auth.py" +test-token-auth = "pytest src/pnnl/goss/core/itests/test_token_auth.py -v" +test-token-auth-standalone = "python src/pnnl/goss/core/itests/test_token_auth.py" + +[dependencies] +"stomp.py" = ">=8.2.0,<9" +"websocket-client" = ">=1.7.0,<2" +pytest = ">=9.0.2,<10" diff --git a/pnnl.goss.core.itests/src/pnnl/goss/core/itests/GossExternalServerTest.java b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/GossExternalServerTest.java new file mode 100644 index 00000000..108e5af7 --- /dev/null +++ b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/GossExternalServerTest.java @@ -0,0 +1,289 @@ +package pnnl.goss.core.itests; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.Serializable; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.auth.UsernamePasswordCredentials; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer; + +import pnnl.goss.core.Client.PROTOCOL; +import pnnl.goss.core.GossResponseEvent; +import pnnl.goss.core.client.GossClient; + +/** + * Integration tests that connect to an already-running GOSS server process. + * + * Unlike {@link GossEndToEndTest} which embeds its own ActiveMQ broker, these + * tests hit a real GOSS server externally — the same way Python/STOMP clients + * (and the companion test_stomp_token_auth.py) connect. + * + * This validates the full stack: ActiveMQ transport, Shiro authentication, + * message routing, and pub/sub — from a GossClient over OpenWire. + * + * Configuration (system properties or environment variables): goss.openwire.uri + * / GOSS_OPENWIRE_URI (default: tcp://localhost:61617) goss.stomp.uri / + * GOSS_STOMP_URI (default: stomp://localhost:61618) goss.username / + * GOSS_USERNAME (default: system) goss.password / GOSS_PASSWORD (default: + * manager) + * + * Run: ./gradlew :pnnl.goss.core.itests:testExternal + */ +@TestInstance(Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class GossExternalServerTest { + + private static final int TEST_TIMEOUT_MS = 10000; + + private String openwireUri; + private String stompUri; + private String username; + private String password; + + /** + * Read config from system properties, falling back to env vars, then defaults. + */ + private static String config(String sysProp, String envVar, String defaultVal) { + String val = System.getProperty(sysProp); + if (val != null && !val.isEmpty()) + return val; + val = System.getenv(envVar); + if (val != null && !val.isEmpty()) + return val; + return defaultVal; + } + + @BeforeAll + public void setUp() { + openwireUri = config("goss.openwire.uri", "GOSS_OPENWIRE_URI", "tcp://localhost:61617"); + stompUri = config("goss.stomp.uri", "GOSS_STOMP_URI", "stomp://localhost:61618"); + username = config("goss.username", "GOSS_USERNAME", "system"); + password = config("goss.password", "GOSS_PASSWORD", "manager"); + + System.out.println("GossExternalServerTest targeting:"); + System.out.println(" OpenWire: " + openwireUri); + System.out.println(" STOMP: " + stompUri); + System.out.println(" User: " + username); + + // Skip the entire class if the server is not reachable + assumeTrue(isServerReachable(), + "GOSS server not reachable at " + openwireUri + " — skipping external tests"); + } + + /** Quick TCP probe to see if the OpenWire port is listening. */ + private boolean isServerReachable() { + try { + String host = openwireUri.replaceAll("^tcp://", "").split(":")[0]; + int port = Integer.parseInt(openwireUri.replaceAll("^tcp://", "").split(":")[1]); + try (Socket s = new Socket(host, port)) { + return true; + } + } catch (Exception e) { + return false; + } + } + + /** Helper: create a connected GossClient with credentials. */ + private GossClient connect() throws Exception { + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password); + GossClient client = new GossClient(PROTOCOL.OPENWIRE, creds, openwireUri, stompUri); + client.createSession(); + return client; + } + + // ------------------------------------------------------------------ + // Tests mirror GossOSGiEndToEndTest + GossEndToEndTest, but against + // an external server process. + // ------------------------------------------------------------------ + + @Test + public void test01_clientConnection() throws Exception { + GossClient client = connect(); + try { + assertNotNull(client.getClientId(), "Client should have an ID"); + System.out.println("Connected with ID: " + client.getClientId()); + } finally { + client.close(); + } + } + + @Test + public void test02_publishSubscribe() throws Exception { + String topic = "test/external/pubsub"; + String message = "Hello from external Java test!"; + + GossClient client = connect(); + try { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + + client.subscribe(topic, new GossResponseEvent() { + @Override + public void onMessage(Serializable response) { + received.set(response.toString()); + latch.countDown(); + } + }); + + Thread.sleep(200); + client.publish(topic, message); + + assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Should receive published message"); + assertTrue(received.get().contains(message), + "Received should contain: " + message + " — got: " + received.get()); + } finally { + client.close(); + } + } + + @Test + public void test03_multipleSubscribers() throws Exception { + String topic = "test/external/multi"; + String message = "Broadcast to all"; + + GossClient publisher = connect(); + GossClient sub1 = connect(); + GossClient sub2 = connect(); + + try { + CountDownLatch latch = new CountDownLatch(2); + AtomicReference msg1 = new AtomicReference<>(); + AtomicReference msg2 = new AtomicReference<>(); + + sub1.subscribe(topic, response -> { + msg1.set(response.toString()); + latch.countDown(); + }); + sub2.subscribe(topic, response -> { + msg2.set(response.toString()); + latch.countDown(); + }); + + Thread.sleep(200); + publisher.publish(topic, message); + + assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Both subscribers should receive"); + assertTrue(msg1.get().contains(message), "Sub1: " + msg1.get()); + assertTrue(msg2.get().contains(message), "Sub2: " + msg2.get()); + } finally { + publisher.close(); + sub1.close(); + sub2.close(); + } + } + + @Test + public void test04_clientReconnection() throws Exception { + // First connection + GossClient client1 = connect(); + String id1 = client1.getClientId(); + client1.close(); + + // Second connection + GossClient client2 = connect(); + String id2 = client2.getClientId(); + + try { + assertNotEquals(id1, id2, "Each connection should have a unique ID"); + + // Verify second client works + String topic = "test/external/reconnect"; + CountDownLatch latch = new CountDownLatch(1); + client2.subscribe(topic, response -> latch.countDown()); + Thread.sleep(100); + client2.publish(topic, "after reconnect"); + + assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Should work after reconnection"); + } finally { + client2.close(); + } + } + + @Test + public void test05_publishJsonData() throws Exception { + String topic = "test/external/json"; + String jsonMessage = "{\"name\":\"test\",\"value\":42}"; + + GossClient client = connect(); + try { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + + client.subscribe(topic, response -> { + received.set(response.toString()); + latch.countDown(); + }); + + Thread.sleep(200); + client.publish(topic, jsonMessage); + + assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Should receive JSON data"); + assertTrue(received.get().contains("test") && received.get().contains("42"), + "JSON should survive round-trip: " + received.get()); + } finally { + client.close(); + } + } + + @Test + public void test06_multipleTopics() throws Exception { + String topicA = "test/external/topicA"; + String topicB = "test/external/topicB"; + + GossClient client = connect(); + try { + CountDownLatch latch = new CountDownLatch(2); + AtomicReference msgA = new AtomicReference<>(); + AtomicReference msgB = new AtomicReference<>(); + + client.subscribe(topicA, response -> { + msgA.set(response.toString()); + latch.countDown(); + }); + client.subscribe(topicB, response -> { + msgB.set(response.toString()); + latch.countDown(); + }); + + Thread.sleep(200); + client.publish(topicA, "Message for A"); + client.publish(topicB, "Message for B"); + + assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Should receive on both topics"); + assertTrue(msgA.get().contains("Message for A"), "Topic A: " + msgA.get()); + assertTrue(msgB.get().contains("Message for B"), "Topic B: " + msgB.get()); + } finally { + client.close(); + } + } + + @Test + public void test07_stompProtocolFallback() throws Exception { + // GossClient with STOMP protocol uses OpenWire internally; verify it works + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password); + GossClient client = new GossClient(PROTOCOL.STOMP, creds, openwireUri, stompUri); + + try { + client.createSession(); + assertNotNull(client.getClientId()); + assertEquals(PROTOCOL.STOMP, client.getProtocol()); + } finally { + client.close(); + } + } +} diff --git a/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_stomp_token_auth.py b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_stomp_token_auth.py new file mode 100644 index 00000000..8e00bf24 --- /dev/null +++ b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_stomp_token_auth.py @@ -0,0 +1,609 @@ +""" +GOSS STOMP Integration Tests + +Python-based integration tests that exercise the GOSS server over STOMP, +covering the same ground as the Java OSGi integration tests +(GossOSGiEndToEndTest) plus token authentication: + + Core connectivity (mirrors GossOSGiEndToEndTest): + - Server is reachable via STOMP + - Publish/subscribe on a topic + - Multiple subscribers receive a broadcast + - Client reconnection with unique sessions + + Token authentication: + - Request a JWT token via the token topic + - Verify the token is non-empty (regression for parseToken bug) + - Reconnect using the token + - Pub/sub works on a token-authenticated connection + - Invalid credentials are rejected + - Empty token is rejected + +Requires a running GOSS server with STOMP transport enabled. +Configure via environment variables or command-line arguments. + +Usage: + # Install dependencies (from pnnl.goss.core.itests/) + pixi install + + # Run via pixi + pixi run test-stomp-token + + # Against default localhost:61618 with system/manager + pixi run test-stomp-token-standalone + + # Against a specific host + pixi run python src/pnnl/goss/core/itests/test_stomp_token_auth.py \\ + --host 192.168.1.10 --port 61618 + + # With custom credentials + pixi run python src/pnnl/goss/core/itests/test_stomp_token_auth.py \\ + --username myuser --password mypass + + # Run with pytest directly + pixi run pytest src/pnnl/goss/core/itests/test_stomp_token_auth.py -v +""" + +import argparse +import base64 +import logging +import os +import sys +import threading +import time +import uuid + +import stomp + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s [%(name)s] %(message)s", +) +log = logging.getLogger("test_stomp_token_auth") + +# --------------------------------------------------------------------------- +# Configuration defaults (overridable via env vars or CLI args) +# --------------------------------------------------------------------------- +STOMP_HOST = os.environ.get("GOSS_STOMP_HOST", "localhost") +STOMP_PORT = int(os.environ.get("GOSS_STOMP_PORT", "61618")) +USERNAME = os.environ.get("GOSS_USERNAME", "system") +PASSWORD = os.environ.get("GOSS_PASSWORD", "manager") +TOKEN_TOPIC = "/topic/pnnl.goss.token.topic" +TOKEN_TIMEOUT_S = 10 +HEARTBEAT_MS = 10000 + + +# --------------------------------------------------------------------------- +# Listener helpers +# --------------------------------------------------------------------------- +class TokenResponseListener(stomp.ConnectionListener): + """Listens on a temporary queue for the JWT token response.""" + + def __init__(self): + self.token = None + self.error = None + self._event = threading.Event() + + def on_message(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.info("Token response received (%d bytes)", len(body)) + self.token = body + self._event.set() + + def on_error(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.error("STOMP error during token request: %s", body) + self.error = body + self._event.set() + + def wait(self, timeout=TOKEN_TIMEOUT_S): + return self._event.wait(timeout) + + +class PubSubListener(stomp.ConnectionListener): + """Listens for a single message on a subscribed topic.""" + + def __init__(self): + self.received_message = None + self.received_headers = None + self.error = None + self._event = threading.Event() + + def on_message(self, frame): + self.received_headers = frame.headers if hasattr(frame, "headers") else {} + self.received_message = frame.body if hasattr(frame, "body") else str(frame) + log.debug("PubSub received: %s", self.received_message[:200]) + self._event.set() + + def on_error(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.error("STOMP error in pub/sub: %s", body) + self.error = body + self._event.set() + + def wait(self, timeout=5): + return self._event.wait(timeout) + + +class MultiMessageListener(stomp.ConnectionListener): + """Collects multiple messages, signaling after a target count is reached.""" + + def __init__(self, target_count=1): + self.messages = [] + self._target = target_count + self._event = threading.Event() + + def on_message(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + self.messages.append(body) + if len(self.messages) >= self._target: + self._event.set() + + def on_error(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.error("STOMP error: %s", body) + + def wait(self, timeout=5): + return self._event.wait(timeout) + + +# --------------------------------------------------------------------------- +# Test helpers +# --------------------------------------------------------------------------- +def create_connection(host, port): + """Create a new STOMP 1.2 connection (not yet connected).""" + return stomp.Connection12( + [(host, port)], + heartbeats=(HEARTBEAT_MS, HEARTBEAT_MS), + ) + + +def request_token(host, port, username, password): + """ + Connect with credentials, request a JWT token, and return it. + + The GOSS token flow: + - Connect via STOMP with username/password + - Subscribe to a temporary reply queue + - Send base64(username:password) to the token topic with reply-to header + - Wait for the server to respond with a JWT token + """ + conn = create_connection(host, port) + listener = TokenResponseListener() + conn.set_listener("token_listener", listener) + + log.info("Connecting to %s:%d as '%s' to request token...", host, port, username) + conn.connect(username, password, wait=True) + assert conn.is_connected(), "Failed to connect with username/password" + log.info("Connected successfully with credentials") + + # Use uuid to avoid reply-queue collisions in rapid test runs + reply_dest = f"temp.token_resp.{username}-{uuid.uuid4().hex[:12]}" + auth_payload = base64.b64encode(f"{username}:{password}".encode()).decode() + + # Subscribe to the temp queue where the token response will arrive + conn.subscribe(destination=f"/queue/{reply_dest}", id="token-sub-1", ack="auto") + log.info("Subscribed to /queue/%s", reply_dest) + + # Allow subscription to propagate to broker before sending + time.sleep(0.3) + + # Send the token request + conn.send( + destination=TOKEN_TOPIC, + body=auth_payload, + headers={"reply-to": f"/queue/{reply_dest}"}, + ) + log.info("Sent token request to %s", TOKEN_TOPIC) + + # Wait for the token + got_response = listener.wait(TOKEN_TIMEOUT_S) + token = listener.token + + # Disconnect the credential-based connection + try: + conn.disconnect() + except Exception as e: + log.debug("Non-critical error during disconnect: %s", e) + + return got_response, token, listener.error + + +def connect_with_token(host, port, token): + """ + Connect using a JWT token as the username with an empty password. + Returns the connected stomp.Connection12. + """ + conn = create_connection(host, port) + log.info("Connecting with token (%d bytes)...", len(token)) + try: + conn.connect(token, "", wait=True) + except stomp.exception.ConnectFailedException as e: + raise AssertionError( + f"Token-based STOMP connect failed. Token length={len(token)}, " + f"token prefix={token[:40]}... Error: {e}" + ) from e + return conn + + +def verify_pubsub_with_token(conn, token): + """ + Verify the token-based connection can publish and subscribe. + Sends a test message on a topic and verifies receipt. + """ + test_topic = "/topic/goss.test.stomp.token.auth" + test_body = f'{{"test": "token_auth", "timestamp": {time.time()}}}' + + listener = PubSubListener() + conn.set_listener("pubsub_listener", listener) + conn.subscribe(destination=test_topic, id="pubsub-sub-1", ack="auto") + log.info("Subscribed to %s", test_topic) + + # Allow subscription to propagate to broker + time.sleep(0.5) + + conn.send( + destination=test_topic, + body=test_body, + headers={"GOSS_HAS_SUBJECT": "true", "GOSS_SUBJECT": token}, + ) + log.info("Published test message to %s", test_topic) + + got_message = listener.wait(5) + return got_message, listener.received_message + + +# --------------------------------------------------------------------------- +# Test cases: Core STOMP functionality (mirrors GossOSGiEndToEndTest) +# --------------------------------------------------------------------------- +class TestStompCore: + """ + Core STOMP integration tests that mirror the Java GossOSGiEndToEndTest. + + These verify the same server behaviors that the OSGi tests check, but + from an external STOMP client rather than from inside the OSGi container. + """ + + host = STOMP_HOST + port = STOMP_PORT + username = USERNAME + password = PASSWORD + + def _connect(self): + """Helper: create and connect a STOMP client with credentials.""" + conn = create_connection(self.host, self.port) + conn.connect(self.username, self.password, wait=True) + assert conn.is_connected(), "Failed to connect" + return conn + + # -- mirrors GossOSGiEndToEndTest.testServerIsRunning / testGossClientConnection -- + def test_server_reachable(self): + """Server accepts STOMP connections (mirrors testServerIsRunning).""" + conn = self._connect() + log.info("PASS: server reachable at %s:%d", self.host, self.port) + conn.disconnect() + + # -- mirrors GossOSGiEndToEndTest.testPublishSubscribe -- + def test_publish_subscribe(self): + """Publish a message and receive it on the same topic (mirrors testPublishSubscribe).""" + conn = self._connect() + test_topic = "/topic/test.stomp.pubsub" + test_message = f'{{"msg": "hello from STOMP", "ts": {time.time()}}}' + + listener = PubSubListener() + conn.set_listener("pubsub", listener) + conn.subscribe(destination=test_topic, id="ps-1", ack="auto") + time.sleep(0.5) + + conn.send(destination=test_topic, body=test_message) + assert listener.wait(5), "Should receive the published message" + assert listener.received_message is not None + assert "hello from STOMP" in listener.received_message, ( + f"Content mismatch: {listener.received_message}" + ) + log.info("PASS: publish/subscribe") + conn.disconnect() + + # -- mirrors GossOSGiEndToEndTest.testMultipleClients -- + def test_multiple_subscribers(self): + """Two subscribers both receive a broadcast (mirrors testMultipleClients).""" + publisher = self._connect() + sub1 = self._connect() + sub2 = self._connect() + + test_topic = "/topic/test.stomp.multi" + test_message = "broadcast message" + + listener1 = PubSubListener() + listener2 = PubSubListener() + sub1.set_listener("sub1", listener1) + sub2.set_listener("sub2", listener2) + sub1.subscribe(destination=test_topic, id="m-1", ack="auto") + sub2.subscribe(destination=test_topic, id="m-2", ack="auto") + time.sleep(0.5) + + publisher.send(destination=test_topic, body=test_message) + + assert listener1.wait(5), "Subscriber 1 should receive the message" + assert listener2.wait(5), "Subscriber 2 should receive the message" + assert test_message in listener1.received_message + assert test_message in listener2.received_message + log.info("PASS: multiple subscribers") + + publisher.disconnect() + sub1.disconnect() + sub2.disconnect() + + # -- mirrors GossOSGiEndToEndTest.testClientReconnection -- + def test_client_reconnection(self): + """Disconnect and reconnect; new session still works (mirrors testClientReconnection).""" + # First connection + conn1 = self._connect() + id1 = conn1.get_listener("") or id(conn1) # no session-id in stomp.py; use object id + conn1.disconnect() + + # Second connection + conn2 = self._connect() + id2 = conn2.get_listener("") or id(conn2) + assert id1 != id2, "Each connection should be a distinct object" + + # Verify second connection can pub/sub + test_topic = "/topic/test.stomp.reconnect" + listener = PubSubListener() + conn2.set_listener("recon", listener) + conn2.subscribe(destination=test_topic, id="r-1", ack="auto") + time.sleep(0.3) + conn2.send(destination=test_topic, body="after reconnect") + assert listener.wait(5), "Should receive message after reconnect" + assert "after reconnect" in listener.received_message + log.info("PASS: client reconnection") + conn2.disconnect() + + def test_publish_json_data(self): + """Publish structured JSON and receive it intact.""" + conn = self._connect() + test_topic = "/topic/test.stomp.json" + import json + payload = json.dumps({"name": "test", "value": 42, "nested": {"a": True}}) + + listener = PubSubListener() + conn.set_listener("json", listener) + conn.subscribe(destination=test_topic, id="j-1", ack="auto") + time.sleep(0.5) + + conn.send(destination=test_topic, body=payload) + assert listener.wait(5), "Should receive JSON message" + received = json.loads(listener.received_message) + assert received["name"] == "test" + assert received["value"] == 42 + assert received["nested"]["a"] is True + log.info("PASS: JSON data round-trip") + conn.disconnect() + + def test_multiple_topics(self): + """Subscribe to two topics and receive messages on both.""" + conn = self._connect() + topic_a = "/topic/test.stomp.topicA" + topic_b = "/topic/test.stomp.topicB" + + listener = MultiMessageListener(target_count=2) + conn.set_listener("multi_topic", listener) + conn.subscribe(destination=topic_a, id="ta-1", ack="auto") + conn.subscribe(destination=topic_b, id="tb-1", ack="auto") + time.sleep(0.5) + + conn.send(destination=topic_a, body="msg for A") + conn.send(destination=topic_b, body="msg for B") + + assert listener.wait(5), "Should receive messages on both topics" + bodies = " ".join(listener.messages) + assert "msg for A" in bodies + assert "msg for B" in bodies + log.info("PASS: multiple topics") + conn.disconnect() + + +# --------------------------------------------------------------------------- +# Test cases: Token authentication +# --------------------------------------------------------------------------- +class TestStompTokenAuth: + """Integration tests for STOMP token authentication against a live GOSS server.""" + + host = STOMP_HOST + port = STOMP_PORT + username = USERNAME + password = PASSWORD + + def test_01_credential_connect(self): + """Verify basic STOMP connection with username/password works.""" + conn = create_connection(self.host, self.port) + conn.connect(self.username, self.password, wait=True) + assert conn.is_connected(), "Should connect with valid credentials" + log.info("PASS: credential connect") + conn.disconnect() + + def test_02_token_request_returns_nonempty_token(self): + """Request a token and verify it is returned non-empty (the core bug fix).""" + got_response, token, error = request_token( + self.host, self.port, self.username, self.password + ) + assert got_response, f"Should get a token response within {TOKEN_TIMEOUT_S}s" + assert error is None, f"Should not get an error: {error}" + assert token is not None, "Token must not be None" + assert len(token.strip()) > 0, "Token must not be empty" + assert token != "authentication failed", "Token request should not fail auth" + # JWT tokens have 3 dot-separated parts (header.payload.signature) + parts = token.split(".") + assert len(parts) == 3, ( + f"Token should be a JWT with 3 parts (header.payload.signature), " + f"got {len(parts)} parts: {token[:80]}..." + ) + log.info("PASS: received valid JWT token (%d bytes, 3 parts)", len(token)) + # Stash for dependent tests + TestStompTokenAuth._token = token + + def test_03_connect_with_token(self): + """Reconnect using the JWT token as credentials.""" + token = getattr(TestStompTokenAuth, "_token", None) + assert token is not None, ( + "test_03 depends on test_02 having produced a valid token. " + "Run tests sequentially: pytest -v (tests are ordered by name)." + ) + + conn = connect_with_token(self.host, self.port, token) + assert conn.is_connected(), "Should connect with token" + log.info("PASS: connected with token") + conn.disconnect() + + def test_04_pubsub_with_token(self): + """Verify publish/subscribe works on a token-authenticated connection.""" + token = getattr(TestStompTokenAuth, "_token", None) + assert token is not None, ( + "test_04 depends on test_02 having produced a valid token. " + "Run tests sequentially: pytest -v (tests are ordered by name)." + ) + + conn = connect_with_token(self.host, self.port, token) + assert conn.is_connected(), "Should connect with token" + + got_message, received = verify_pubsub_with_token(conn, token) + assert got_message, "Should receive the published message" + assert received is not None, "Received message should not be None" + assert "token_auth" in received, f"Message content mismatch: {received}" + log.info("PASS: pub/sub works with token auth") + conn.disconnect() + + def test_05_invalid_credentials_no_token(self): + """Verify that invalid credentials do not produce a valid token.""" + try: + got_response, token, error = request_token( + self.host, self.port, "baduser", "badpass" + ) + except (stomp.exception.ConnectFailedException, AssertionError): + # Connection refused with bad credentials -- expected + log.info("PASS: invalid credentials rejected at STOMP connect") + return + + # If connection succeeded, verify no valid JWT was issued + if got_response and token: + assert token == "authentication failed" or len(token.split(".")) != 3, ( + f"Invalid credentials should not produce a valid JWT, got: {token[:80]}" + ) + log.info("PASS: server returned auth failure message") + else: + # No response within timeout -- also acceptable (server ignored bad creds) + log.info("PASS: no token response for invalid credentials (timeout)") + + def test_06_empty_token_rejected(self): + """Verify that connecting with an empty string as token fails.""" + conn = create_connection(self.host, self.port) + connected = False + try: + conn.connect("", "", wait=True, headers={"accept-version": "1.2"}) + connected = conn.is_connected() + except (stomp.exception.ConnectFailedException, Exception): + log.info("PASS: empty token connection correctly refused") + return + finally: + try: + conn.disconnect() + except Exception: + pass + + if connected: + # Broker allows anonymous -- this test is not meaningful in + # that configuration, so skip rather than false-pass. + try: + import pytest + pytest.skip( + "Broker allows anonymous connections; " + "empty-token rejection cannot be verified." + ) + except ImportError: + log.warning( + "SKIP: broker allows anonymous connections; " + "empty-token rejection cannot be verified." + ) + + +# --------------------------------------------------------------------------- +# Standalone runner +# --------------------------------------------------------------------------- +def run_all_tests(host, port, username, password): + """Run all tests sequentially, reporting pass/fail.""" + test_classes = [TestStompCore, TestStompTokenAuth] + passed = 0 + failed = 0 + skipped = 0 + errors = [] + + print(f"\n{'='*60}") + print(f"GOSS STOMP Integration Tests") + print(f"Server: {host}:{port} User: {username}") + print(f"{'='*60}") + + for cls in test_classes: + instance = cls() + instance.host = host + instance.port = port + instance.username = username + instance.password = password + + test_methods = [m for m in sorted(dir(instance)) if m.startswith("test_")] + print(f"\n [{cls.__name__}]") + + for method_name in test_methods: + method = getattr(instance, method_name) + doc = method.__doc__ or method_name + print(f" {method_name}: {doc.strip()}") + try: + method() + passed += 1 + print(f" -> PASSED") + except Exception as e: + if "SKIP" in str(e) or "skip" in type(e).__name__.lower(): + skipped += 1 + print(f" -> SKIPPED: {e}") + else: + failed += 1 + errors.append((f"{cls.__name__}.{method_name}", e)) + print(f" -> FAILED: {e}") + + print(f"\n{'='*60}") + print(f"Results: {passed} passed, {failed} failed, {skipped} skipped " + f"out of {passed + failed + skipped}") + print(f"{'='*60}") + + if errors: + print("\nFailures:") + for name, err in errors: + print(f" {name}: {err}") + return 1 + return 0 + + +def main(): + parser = argparse.ArgumentParser( + description="Test STOMP token authentication against a GOSS server" + ) + parser.add_argument( + "--host", default=STOMP_HOST, help=f"STOMP host (default: {STOMP_HOST})" + ) + parser.add_argument( + "--port", type=int, default=STOMP_PORT, + help=f"STOMP port (default: {STOMP_PORT})" + ) + parser.add_argument( + "--username", default=USERNAME, help=f"Username (default: {USERNAME})" + ) + parser.add_argument( + "--password", default=PASSWORD, help=f"Password (default: {PASSWORD})" + ) + args = parser.parse_args() + + rc = run_all_tests(args.host, args.port, args.username, args.password) + sys.exit(rc) + + +if __name__ == "__main__": + main() diff --git a/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_token_auth.py b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_token_auth.py new file mode 100644 index 00000000..fe27d967 --- /dev/null +++ b/pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_token_auth.py @@ -0,0 +1,796 @@ +""" +GOSS Token Authentication Tests -- STOMP and WebSocket + +Integration tests that exercise JWT token authentication over both +STOMP (TCP) and WebSocket transports against a running GOSS server. + +Test matrix: + For each transport (STOMP, WebSocket): + - Connect with username/password credentials + - Request a JWT token via the token topic + - Validate the JWT has 3 parts (header.payload.signature) + - Reconnect using the JWT token + - Publish/subscribe works on the token-authenticated connection + - Invalid credentials are rejected + - Empty token is rejected + +Requires a running GOSS server with STOMP and WebSocket transports enabled. +Configure via environment variables or command-line arguments. + +Usage: + # Activate the virtual environment + source pnnl.goss.core.itests/.venv/bin/activate + + # Run all tests + pytest pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_token_auth.py -v + + # Run only WebSocket tests + pytest ... -k "Ws" + + # Run only STOMP tests + pytest ... -k "Stomp" + + # Standalone (no pytest required) + python pnnl.goss.core.itests/src/pnnl/goss/core/itests/test_token_auth.py + + # Custom host/ports + python ... --stomp-host localhost --stomp-port 61618 --ws-port 61616 +""" + +import argparse +import base64 +import json +import logging +import os +import sys +import threading +import time +import uuid + +import stomp +from stomp.adapter.ws import WSStompConnection + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s [%(name)s] %(message)s", +) +log = logging.getLogger("test_token_auth") + +# --------------------------------------------------------------------------- +# Configuration defaults (overridable via env vars or CLI args) +# --------------------------------------------------------------------------- +STOMP_HOST = os.environ.get("GOSS_STOMP_HOST", "localhost") +STOMP_PORT = int(os.environ.get("GOSS_STOMP_PORT", "61618")) +WS_PORT = int(os.environ.get("GOSS_WS_PORT", "61616")) +USERNAME = os.environ.get("GOSS_USERNAME", "craig") +PASSWORD = os.environ.get("GOSS_PASSWORD", "craig") +TOKEN_TOPIC = "/topic/pnnl.goss.token.topic" +TOKEN_TIMEOUT_S = 10 +HEARTBEAT_MS = 10000 + + +# --------------------------------------------------------------------------- +# Listener helpers +# --------------------------------------------------------------------------- +class TokenResponseListener(stomp.ConnectionListener): + """Listens on a temporary queue for the JWT token response.""" + + def __init__(self): + self.token = None + self.error = None + self._event = threading.Event() + + def on_message(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.info("Token response received (%d bytes)", len(body)) + self.token = body + self._event.set() + + def on_error(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.error("STOMP error during token request: %s", body) + self.error = body + self._event.set() + + def wait(self, timeout=TOKEN_TIMEOUT_S): + return self._event.wait(timeout) + + +class PubSubListener(stomp.ConnectionListener): + """Listens for a single message on a subscribed topic.""" + + def __init__(self): + self.received_message = None + self.received_headers = None + self.error = None + self._event = threading.Event() + + def on_message(self, frame): + self.received_headers = frame.headers if hasattr(frame, "headers") else {} + self.received_message = frame.body if hasattr(frame, "body") else str(frame) + log.debug("PubSub received: %s", self.received_message[:200]) + self._event.set() + + def on_error(self, frame): + body = frame.body if hasattr(frame, "body") else str(frame) + log.error("STOMP error in pub/sub: %s", body) + self.error = body + self._event.set() + + def wait(self, timeout=5): + return self._event.wait(timeout) + + +# --------------------------------------------------------------------------- +# Connection factories +# --------------------------------------------------------------------------- +def create_stomp_connection(host, port): + """Create a new STOMP 1.2 connection over TCP (not yet connected).""" + return stomp.Connection12( + [(host, port)], + heartbeats=(HEARTBEAT_MS, HEARTBEAT_MS), + ) + + +def create_ws_connection(host, port): + """Create a new STOMP 1.2 connection over WebSocket (not yet connected).""" + return WSStompConnection( + [(host, port)], + heartbeats=(HEARTBEAT_MS, HEARTBEAT_MS), + header=["Sec-WebSocket-Protocol: v12.stomp"], + ) + + +# --------------------------------------------------------------------------- +# Token helpers +# --------------------------------------------------------------------------- +def request_token(conn_factory, host, port, username, password): + """ + Connect with credentials, request a JWT token, and return it. + + The GOSS token flow: + 1. Connect via STOMP with username/password + 2. Subscribe to a temporary reply queue + 3. Send base64(username:password) to the token topic with reply-to header + 4. Wait for the server to respond with a JWT token + """ + conn = conn_factory(host, port) + listener = TokenResponseListener() + conn.set_listener("token_listener", listener) + + log.info("Connecting to %s:%d as '%s' to request token...", host, port, username) + conn.connect(username, password, wait=True) + assert conn.is_connected(), "Failed to connect with username/password" + log.info("Connected successfully with credentials") + + reply_dest = f"temp.token_resp.{username}-{uuid.uuid4().hex[:12]}" + auth_payload = base64.b64encode(f"{username}:{password}".encode()).decode() + + conn.subscribe(destination=f"/queue/{reply_dest}", id="token-sub-1", ack="auto") + log.info("Subscribed to /queue/%s", reply_dest) + + # Allow subscription to propagate to broker + time.sleep(0.3) + + conn.send( + destination=TOKEN_TOPIC, + body=auth_payload, + headers={"reply-to": f"/queue/{reply_dest}"}, + ) + log.info("Sent token request to %s", TOKEN_TOPIC) + + got_response = listener.wait(TOKEN_TIMEOUT_S) + token = listener.token + + try: + conn.disconnect() + except Exception as e: + log.debug("Non-critical error during disconnect: %s", e) + + return got_response, token, listener.error + + +def request_token_bare_reply(conn_factory, host, port, username, password): + """ + Same as request_token but sends reply-to WITHOUT the /queue/ prefix. + + This tests that the server correctly handles bare destination names + (e.g., "temp.token_resp.xyz" instead of "/queue/temp.token_resp.xyz"). + """ + conn = conn_factory(host, port) + listener = TokenResponseListener() + conn.set_listener("token_listener", listener) + + log.info("Connecting to %s:%d as '%s' to request token (bare reply-to)...", + host, port, username) + conn.connect(username, password, wait=True) + assert conn.is_connected(), "Failed to connect with username/password" + + bare_dest = f"temp.token_resp.bare.{username}-{uuid.uuid4().hex[:12]}" + auth_payload = base64.b64encode(f"{username}:{password}".encode()).decode() + + # Subscribe using /queue/ prefix (STOMP requires it for subscription) + conn.subscribe(destination=f"/queue/{bare_dest}", id="token-sub-bare", ack="auto") + log.info("Subscribed to /queue/%s", bare_dest) + + time.sleep(0.3) + + # Send with bare reply-to (no /queue/ prefix) + conn.send( + destination=TOKEN_TOPIC, + body=auth_payload, + headers={"reply-to": bare_dest}, + ) + log.info("Sent token request with bare reply-to: %s", bare_dest) + + got_response = listener.wait(TOKEN_TIMEOUT_S) + token = listener.token + + try: + conn.disconnect() + except Exception as e: + log.debug("Non-critical error during disconnect: %s", e) + + return got_response, token, listener.error + + +def connect_with_token(conn_factory, host, port, token): + """Connect using a JWT token as the username with an empty password.""" + conn = conn_factory(host, port) + log.info("Connecting with token (%d bytes)...", len(token)) + try: + conn.connect(token, "", wait=True) + except stomp.exception.ConnectFailedException as e: + raise AssertionError( + f"Token-based connect failed. Token length={len(token)}, " + f"token prefix={token[:40]}... Error: {e}" + ) from e + return conn + + +def verify_pubsub(conn, token): + """Verify the connection can publish and subscribe on a test topic.""" + test_topic = "/topic/goss.test.token.auth" + test_body = json.dumps({"test": "token_auth", "timestamp": time.time()}) + + listener = PubSubListener() + conn.set_listener("pubsub_listener", listener) + conn.subscribe(destination=test_topic, id="pubsub-sub-1", ack="auto") + log.info("Subscribed to %s", test_topic) + + time.sleep(0.5) + + conn.send( + destination=test_topic, + body=test_body, + headers={"GOSS_HAS_SUBJECT": "true", "GOSS_SUBJECT": token}, + ) + log.info("Published test message to %s", test_topic) + + got_message = listener.wait(5) + return got_message, listener.received_message + + +def verify_queue(conn, token): + """Verify the connection can send to and consume from a queue.""" + queue_name = f"/queue/goss.test.token.queue.{uuid.uuid4().hex[:8]}" + test_body = json.dumps({"test": "queue_auth", "timestamp": time.time()}) + + listener = PubSubListener() + conn.set_listener("queue_listener", listener) + conn.subscribe(destination=queue_name, id="queue-sub-1", ack="auto") + log.info("Subscribed to queue %s", queue_name) + + time.sleep(0.5) + + conn.send( + destination=queue_name, + body=test_body, + headers={"GOSS_HAS_SUBJECT": "true", "GOSS_SUBJECT": token}, + ) + log.info("Sent message to queue %s", queue_name) + + got_message = listener.wait(5) + return got_message, listener.received_message + + +def verify_queue_request_response(sender_conn, receiver_conn, token): + """ + Two-client queue request/response: sender publishes to a queue, + receiver consumes from it, then replies on a response queue. + """ + request_queue = f"/queue/goss.test.request.{uuid.uuid4().hex[:8]}" + response_queue = f"/queue/goss.test.response.{uuid.uuid4().hex[:8]}" + request_body = json.dumps({"request": "ping", "ts": time.time()}) + response_body = json.dumps({"response": "pong", "ts": time.time()}) + + # Receiver subscribes to the request queue + req_listener = PubSubListener() + receiver_conn.set_listener("req_listener", req_listener) + receiver_conn.subscribe(destination=request_queue, id="rr-req-1", ack="auto") + + # Sender subscribes to the response queue + resp_listener = PubSubListener() + sender_conn.set_listener("resp_listener", resp_listener) + sender_conn.subscribe(destination=response_queue, id="rr-resp-1", ack="auto") + + time.sleep(0.5) + + # Sender sends request with reply-to header + sender_conn.send( + destination=request_queue, + body=request_body, + headers={"reply-to": response_queue}, + ) + log.info("Sent request to %s", request_queue) + + # Receiver gets the request + got_request = req_listener.wait(5) + if not got_request: + return False, None, None + + # Receiver sends response to reply-to queue + receiver_conn.send(destination=response_queue, body=response_body) + log.info("Sent response to %s", response_queue) + + # Sender gets the response + got_response = resp_listener.wait(5) + return got_response, req_listener.received_message, resp_listener.received_message + + +# =========================================================================== +# STOMP (TCP) Token Auth Tests +# =========================================================================== +class TestStompTokenAuth: + """Token authentication tests over STOMP TCP transport.""" + + host = STOMP_HOST + port = STOMP_PORT + username = USERNAME + password = PASSWORD + _token = None + + def _factory(self): + return create_stomp_connection + + def test_01_credential_connect(self): + """Connect with username/password over STOMP.""" + conn = self._factory()(self.host, self.port) + conn.connect(self.username, self.password, wait=True) + assert conn.is_connected(), "Should connect with valid credentials" + log.info("STOMP: PASS credential connect") + conn.disconnect() + + def test_02_request_token(self): + """Request a JWT token over STOMP and validate structure.""" + got_response, token, error = request_token( + self._factory(), self.host, self.port, self.username, self.password + ) + assert got_response, f"Should get a token response within {TOKEN_TIMEOUT_S}s" + assert error is None, f"Should not get an error: {error}" + assert token is not None, "Token must not be None" + assert len(token.strip()) > 0, "Token must not be empty" + assert token != "authentication failed", "Token request should not fail auth" + + parts = token.split(".") + assert len(parts) == 3, ( + f"Token should be a JWT with 3 parts, got {len(parts)}: {token[:80]}..." + ) + log.info("STOMP: PASS received valid JWT (%d bytes)", len(token)) + TestStompTokenAuth._token = token + + def test_02b_request_token_bare_reply_to(self): + """Request a JWT token with bare reply-to (no /queue/ prefix) over STOMP.""" + got_response, token, error = request_token_bare_reply( + self._factory(), self.host, self.port, self.username, self.password + ) + assert got_response, ( + f"Should get a token response with bare reply-to within {TOKEN_TIMEOUT_S}s" + ) + assert error is None, f"Should not get an error: {error}" + assert token is not None, "Token must not be None" + assert len(token.strip()) > 0, "Token must not be empty" + assert token != "authentication failed", "Token request should not fail auth" + + parts = token.split(".") + assert len(parts) == 3, ( + f"Token should be a JWT with 3 parts, got {len(parts)}: {token[:80]}..." + ) + log.info("STOMP: PASS bare reply-to token request (%d bytes)", len(token)) + + def test_03_connect_with_token(self): + """Reconnect using the JWT token over STOMP.""" + token = TestStompTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected(), "Should connect with token" + log.info("STOMP: PASS connected with token") + conn.disconnect() + + def test_04_pubsub_with_token(self): + """Publish/subscribe on a topic works with token auth over STOMP.""" + token = TestStompTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected() + + got_message, received = verify_pubsub(conn, token) + assert got_message, "Should receive the published message" + assert received is not None + assert "token_auth" in received, f"Content mismatch: {received}" + log.info("STOMP: PASS topic pub/sub with token auth") + conn.disconnect() + + def test_04b_queue_with_token(self): + """Send/receive on a queue works with token auth over STOMP.""" + token = TestStompTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected() + + got_message, received = verify_queue(conn, token) + assert got_message, "Should receive the queue message" + assert received is not None + assert "queue_auth" in received, f"Content mismatch: {received}" + log.info("STOMP: PASS queue send/receive with token auth") + conn.disconnect() + + def test_05_invalid_credentials(self): + """Invalid credentials do not produce a valid token over STOMP.""" + try: + got_response, token, error = request_token( + self._factory(), self.host, self.port, "baduser", "badpass" + ) + except (stomp.exception.ConnectFailedException, AssertionError): + log.info("STOMP: PASS invalid credentials rejected at connect") + return + + if got_response and token: + assert token == "authentication failed" or len(token.split(".")) != 3, ( + f"Invalid credentials should not produce a valid JWT: {token[:80]}" + ) + log.info("STOMP: PASS server returned auth failure") + else: + log.info("STOMP: PASS no token for invalid credentials (timeout)") + + def test_06_empty_token_rejected(self): + """Empty string as token is rejected over STOMP.""" + conn = self._factory()(self.host, self.port) + try: + conn.connect("", "", wait=True, headers={"accept-version": "1.2"}) + connected = conn.is_connected() + except (stomp.exception.ConnectFailedException, Exception): + log.info("STOMP: PASS empty token rejected") + return + finally: + try: + conn.disconnect() + except Exception: + pass + + if connected: + try: + import pytest + pytest.skip("Broker allows anonymous connections") + except ImportError: + log.warning("SKIP: broker allows anonymous connections") + + +# =========================================================================== +# WebSocket Token Auth Tests +# =========================================================================== +class TestWsTokenAuth: + """Token authentication tests over WebSocket transport.""" + + host = STOMP_HOST + port = WS_PORT + username = USERNAME + password = PASSWORD + _token = None + + def _factory(self): + return create_ws_connection + + def test_01_credential_connect(self): + """Connect with username/password over WebSocket.""" + conn = self._factory()(self.host, self.port) + conn.connect(self.username, self.password, wait=True) + assert conn.is_connected(), "Should connect with valid credentials" + log.info("WS: PASS credential connect") + conn.disconnect() + + def test_02_request_token(self): + """Request a JWT token over WebSocket and validate structure.""" + got_response, token, error = request_token( + self._factory(), self.host, self.port, self.username, self.password + ) + assert got_response, f"Should get a token response within {TOKEN_TIMEOUT_S}s" + assert error is None, f"Should not get an error: {error}" + assert token is not None, "Token must not be None" + assert len(token.strip()) > 0, "Token must not be empty" + assert token != "authentication failed", "Token request should not fail auth" + + parts = token.split(".") + assert len(parts) == 3, ( + f"Token should be a JWT with 3 parts, got {len(parts)}: {token[:80]}..." + ) + log.info("WS: PASS received valid JWT (%d bytes)", len(token)) + TestWsTokenAuth._token = token + + def test_02b_request_token_bare_reply_to(self): + """Request a JWT token with bare reply-to (no /queue/ prefix) over WebSocket.""" + got_response, token, error = request_token_bare_reply( + self._factory(), self.host, self.port, self.username, self.password + ) + assert got_response, ( + f"Should get a token response with bare reply-to within {TOKEN_TIMEOUT_S}s" + ) + assert error is None, f"Should not get an error: {error}" + assert token is not None, "Token must not be None" + assert len(token.strip()) > 0, "Token must not be empty" + assert token != "authentication failed", "Token request should not fail auth" + + parts = token.split(".") + assert len(parts) == 3, ( + f"Token should be a JWT with 3 parts, got {len(parts)}: {token[:80]}..." + ) + log.info("WS: PASS bare reply-to token request (%d bytes)", len(token)) + + def test_03_connect_with_token(self): + """Reconnect using the JWT token over WebSocket.""" + token = TestWsTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected(), "Should connect with token" + log.info("WS: PASS connected with token") + conn.disconnect() + + def test_04_pubsub_with_token(self): + """Publish/subscribe on a topic works with token auth over WebSocket.""" + token = TestWsTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected() + + got_message, received = verify_pubsub(conn, token) + assert got_message, "Should receive the published message" + assert received is not None + assert "token_auth" in received, f"Content mismatch: {received}" + log.info("WS: PASS topic pub/sub with token auth") + conn.disconnect() + + def test_04b_queue_with_token(self): + """Send/receive on a queue works with token auth over WebSocket.""" + token = TestWsTokenAuth._token + assert token is not None, "Depends on test_02 having produced a valid token" + + conn = connect_with_token(self._factory(), self.host, self.port, token) + assert conn.is_connected() + + got_message, received = verify_queue(conn, token) + assert got_message, "Should receive the queue message" + assert received is not None + assert "queue_auth" in received, f"Content mismatch: {received}" + log.info("WS: PASS queue send/receive with token auth") + conn.disconnect() + + def test_05_invalid_credentials(self): + """Invalid credentials do not produce a valid token over WebSocket.""" + try: + got_response, token, error = request_token( + self._factory(), self.host, self.port, "baduser", "badpass" + ) + except (stomp.exception.ConnectFailedException, AssertionError): + log.info("WS: PASS invalid credentials rejected at connect") + return + + if got_response and token: + assert token == "authentication failed" or len(token.split(".")) != 3, ( + f"Invalid credentials should not produce a valid JWT: {token[:80]}" + ) + log.info("WS: PASS server returned auth failure") + else: + log.info("WS: PASS no token for invalid credentials (timeout)") + + def test_06_empty_token_rejected(self): + """Empty string as token is rejected over WebSocket.""" + conn = self._factory()(self.host, self.port) + try: + conn.connect("", "", wait=True, headers={"accept-version": "1.2"}) + connected = conn.is_connected() + except Exception: + log.info("WS: PASS empty token rejected") + return + finally: + try: + conn.disconnect() + except Exception: + pass + + if connected: + try: + import pytest + pytest.skip("Broker allows anonymous connections") + except ImportError: + log.warning("SKIP: broker allows anonymous connections") + + +# =========================================================================== +# Cross-transport test: obtain token via STOMP, use via WebSocket (and vice versa) +# =========================================================================== +class TestCrossTransportToken: + """Verify a token obtained on one transport works on the other.""" + + host = STOMP_HOST + stomp_port = STOMP_PORT + ws_port = WS_PORT + username = USERNAME + password = PASSWORD + + def test_01_stomp_token_on_ws(self): + """Token obtained via STOMP connects over WebSocket.""" + got_response, token, error = request_token( + create_stomp_connection, self.host, self.stomp_port, + self.username, self.password, + ) + assert got_response and token and len(token.split(".")) == 3, ( + f"Failed to obtain token via STOMP: {error}" + ) + + conn = connect_with_token(create_ws_connection, self.host, self.ws_port, token) + assert conn.is_connected(), "STOMP token should work on WebSocket" + got_message, received = verify_pubsub(conn, token) + assert got_message, "Should receive message over WS with STOMP-issued token" + assert "token_auth" in received + log.info("CROSS: PASS STOMP token works on WebSocket") + conn.disconnect() + + def test_02_ws_token_on_stomp(self): + """Token obtained via WebSocket connects over STOMP.""" + got_response, token, error = request_token( + create_ws_connection, self.host, self.ws_port, + self.username, self.password, + ) + assert got_response and token and len(token.split(".")) == 3, ( + f"Failed to obtain token via WebSocket: {error}" + ) + + conn = connect_with_token( + create_stomp_connection, self.host, self.stomp_port, token, + ) + assert conn.is_connected(), "WS token should work on STOMP" + got_message, received = verify_pubsub(conn, token) + assert got_message, "Should receive message over STOMP with WS-issued token" + assert "token_auth" in received + log.info("CROSS: PASS WebSocket token works on STOMP") + conn.disconnect() + + def test_03_queue_request_response_cross_transport(self): + """Queue request/response: STOMP sender, WebSocket receiver.""" + # Get a token (use STOMP to request) + got_response, token, error = request_token( + create_stomp_connection, self.host, self.stomp_port, + self.username, self.password, + ) + assert got_response and token and len(token.split(".")) == 3 + + sender = connect_with_token( + create_stomp_connection, self.host, self.stomp_port, token, + ) + receiver = connect_with_token( + create_ws_connection, self.host, self.ws_port, token, + ) + assert sender.is_connected() and receiver.is_connected() + + got_resp, req_msg, resp_msg = verify_queue_request_response( + sender, receiver, token, + ) + assert got_resp, "Should complete queue request/response cycle" + assert req_msg is not None and "ping" in req_msg + assert resp_msg is not None and "pong" in resp_msg + log.info("CROSS: PASS queue request/response (STOMP->WS)") + sender.disconnect() + receiver.disconnect() + + +# --------------------------------------------------------------------------- +# Standalone runner +# --------------------------------------------------------------------------- +def run_all_tests(host, stomp_port, ws_port, username, password): + """Run all tests sequentially, reporting pass/fail.""" + test_classes = [ + ("STOMP Token Auth", TestStompTokenAuth, {"host": host, "port": stomp_port}), + ("WebSocket Token Auth", TestWsTokenAuth, {"host": host, "port": ws_port}), + ("Cross-Transport Token", TestCrossTransportToken, + {"host": host, "stomp_port": stomp_port, "ws_port": ws_port}), + ] + passed = 0 + failed = 0 + skipped = 0 + errors = [] + + print(f"\n{'='*65}") + print(f"GOSS Token Authentication Tests (STOMP + WebSocket)") + print(f" STOMP: {host}:{stomp_port}") + print(f" WebSocket: {host}:{ws_port}") + print(f" User: {username}") + print(f"{'='*65}") + + for label, cls, attrs in test_classes: + instance = cls() + instance.username = username + instance.password = password + for k, v in attrs.items(): + setattr(instance, k, v) + + test_methods = [m for m in sorted(dir(instance)) if m.startswith("test_")] + print(f"\n [{label}]") + + for method_name in test_methods: + method = getattr(instance, method_name) + doc = method.__doc__ or method_name + print(f" {method_name}: {doc.strip()}") + try: + method() + passed += 1 + print(f" -> PASSED") + except Exception as e: + if "SKIP" in str(e) or "skip" in type(e).__name__.lower(): + skipped += 1 + print(f" -> SKIPPED: {e}") + else: + failed += 1 + errors.append((f"{label}.{method_name}", e)) + print(f" -> FAILED: {e}") + + print(f"\n{'='*65}") + print(f"Results: {passed} passed, {failed} failed, {skipped} skipped " + f"out of {passed + failed + skipped}") + print(f"{'='*65}") + + if errors: + print("\nFailures:") + for name, err in errors: + print(f" {name}: {err}") + return 1 + return 0 + + +def main(): + parser = argparse.ArgumentParser( + description="Test JWT token auth over STOMP and WebSocket against a GOSS server" + ) + parser.add_argument( + "--stomp-host", default=STOMP_HOST, + help=f"Server host (default: {STOMP_HOST})" + ) + parser.add_argument( + "--stomp-port", type=int, default=STOMP_PORT, + help=f"STOMP port (default: {STOMP_PORT})" + ) + parser.add_argument( + "--ws-port", type=int, default=WS_PORT, + help=f"WebSocket port (default: {WS_PORT})" + ) + parser.add_argument( + "--username", default=USERNAME, help=f"Username (default: {USERNAME})" + ) + parser.add_argument( + "--password", default=PASSWORD, help=f"Password (default: {PASSWORD})" + ) + args = parser.parse_args() + + rc = run_all_tests( + args.stomp_host, args.stomp_port, args.ws_port, + args.username, args.password, + ) + sys.exit(rc) + + +if __name__ == "__main__": + main() diff --git a/pnnl.goss.core.runner/.gitignore b/pnnl.goss.core.runner/.gitignore index e7cf945e..61735c10 100644 --- a/pnnl.goss.core.runner/.gitignore +++ b/pnnl.goss.core.runner/.gitignore @@ -1,4 +1 @@ -/bin/ -/bin_test/ -/generated/ -/log/ +# Covered by root .gitignore diff --git a/pnnl.goss.core.runner/bnd.bnd b/pnnl.goss.core.runner/bnd.bnd index f439fa7d..ceec7dc8 100644 --- a/pnnl.goss.core.runner/bnd.bnd +++ b/pnnl.goss.core.runner/bnd.bnd @@ -1,4 +1,4 @@ -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 -buildpath: \ org.apache.felix.gogo.command,\ org.apache.felix.gogo.runtime,\ diff --git a/pnnl.goss.core.runner/build.gradle b/pnnl.goss.core.runner/build.gradle index 137e2921..faab2e72 100644 --- a/pnnl.goss.core.runner/build.gradle +++ b/pnnl.goss.core.runner/build.gradle @@ -14,9 +14,13 @@ bndRunner { dependencies { implementation project(':pnnl.goss.core') - // For simple runner - implementation 'org.apache.activemq:activemq-broker:5.15.16' + // For simple runner (ActiveMQ 6.x uses jakarta.jms) + implementation 'org.apache.activemq:activemq-broker:6.2.0' + implementation 'org.apache.activemq:activemq-shiro:6.2.0' + implementation 'org.apache.activemq:activemq-http:6.2.0' // WebSocket transport (Jetty) + implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.26' implementation 'org.apache.shiro:shiro-core:1.13.0' + implementation 'com.nimbusds:nimbus-jose-jwt:9.31' } // Simple executable JAR - no OSGi complexity @@ -35,10 +39,12 @@ task createSimpleRunner(type: Jar) { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - + from sourceSets.main.output from project(':pnnl.goss.core').sourceSets.main.output - + + // Exclude signature files from signed JARs to avoid fat JAR conflicts + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' duplicatesStrategy = DuplicatesStrategy.EXCLUDE } @@ -61,6 +67,7 @@ task createSSLRunner(type: Jar) { from sourceSets.main.output from project(':pnnl.goss.core').sourceSets.main.output + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' duplicatesStrategy = DuplicatesStrategy.EXCLUDE } @@ -84,6 +91,7 @@ task createCli(type: Jar) { from sourceSets.main.output from project(':pnnl.goss.core').sourceSets.main.output + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' duplicatesStrategy = DuplicatesStrategy.EXCLUDE } @@ -173,7 +181,7 @@ createSimpleRunner.dependsOn(':pnnl.goss.core:jar', 'jar') createSSLRunner.dependsOn(':pnnl.goss.core:jar', 'jar') createCli.dependsOn(':pnnl.goss.core:jar', 'jar') -build.dependsOn createGossRunner, createGossSSLRunner, createSimpleRunner, createSSLRunner, createCli +assemble.dependsOn createGossRunner, createGossSSLRunner, createSimpleRunner, createSSLRunner, createCli // Note: Generic buildRunner. tasks are now provided by BndRunnerPlugin // Usage: ./gradlew buildRunner.goss-core (builds from goss-core.bndrun) diff --git a/pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg b/pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg index 1a1096a8..478641e1 100644 --- a/pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg +++ b/pnnl.goss.core.runner/conf/pnnl.goss.core.security.propertyfile.cfg @@ -1,2 +1,3 @@ +system=manager,queue:*,topic:*,temp-queue:* craig=craig,queue:*,topic:*,temp-queue:* -july=july,queue:,topic:*,temp-queue:* \ No newline at end of file +july=july,queue:*,topic:*,temp-queue:* diff --git a/pnnl.goss.core.runner/goss-core-ssl.bndrun b/pnnl.goss.core.runner/goss-core-ssl.bndrun index bbba43a1..7b286381 100644 --- a/pnnl.goss.core.runner/goss-core-ssl.bndrun +++ b/pnnl.goss.core.runner/goss-core-ssl.bndrun @@ -10,6 +10,7 @@ ${javax-runpath},\ ${configadmin-runpath},\ ${fileinstall-runpath},\ + ${nimbus-jose-jwt-runpath},\ ${gogo-runpath},\ ${scr-runpath},\ ${pax-logging-runpath},\ diff --git a/pnnl.goss.core.runner/goss-core.bndrun b/pnnl.goss.core.runner/goss-core.bndrun index d53e16bd..265d0bd8 100644 --- a/pnnl.goss.core.runner/goss-core.bndrun +++ b/pnnl.goss.core.runner/goss-core.bndrun @@ -14,6 +14,7 @@ ${jakarta-runpath},\ ${configadmin-runpath},\ ${fileinstall-runpath},\ + ${nimbus-jose-jwt-runpath},\ pnnl.goss.core.core-api;version=latest,\ pnnl.goss.core.goss-client;version=latest,\ pnnl.goss.core.goss-core-commands;version=latest,\ diff --git a/pnnl.goss.core.runner/src/main/java/pnnl/goss/core/runner/GossSimpleRunner.java b/pnnl.goss.core.runner/src/main/java/pnnl/goss/core/runner/GossSimpleRunner.java index 4d98357e..03a98684 100644 --- a/pnnl.goss.core.runner/src/main/java/pnnl/goss/core/runner/GossSimpleRunner.java +++ b/pnnl.goss.core.runner/src/main/java/pnnl/goss/core/runner/GossSimpleRunner.java @@ -1,18 +1,106 @@ package pnnl.goss.core.runner; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.net.URI; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.jms.BytesMessage; +import jakarta.jms.Connection; +import jakarta.jms.Destination; +import jakarta.jms.Message; +import jakarta.jms.MessageConsumer; +import jakarta.jms.MessageListener; +import jakarta.jms.MessageProducer; +import jakarta.jms.Session; +import jakarta.jms.TextMessage; +import jakarta.jms.Topic; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.broker.BrokerPlugin; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.broker.TransportConnector; +import org.apache.activemq.shiro.ShiroPlugin; +import org.apache.activemq.shiro.mgt.DefaultActiveMqSecurityManager; import org.apache.activemq.usage.SystemUsage; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.SimpleAccount; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.permission.PermissionResolver; +import org.apache.shiro.cache.MemoryConstrainedCacheManager; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.subject.PrincipalCollection; -import java.net.URI; +import pnnl.goss.core.GossCoreContants; +import pnnl.goss.core.security.JWTAuthenticationToken; +import pnnl.goss.core.security.SecurityConfig; +import pnnl.goss.core.security.impl.SecurityConfigImpl; +import pnnl.goss.core.security.impl.GossWildcardPermissionResolver; /** - * Simple GOSS Runner - No OSGi, just plain Java This bypasses all the OSGi - * complexity and just starts the core services + * GOSS Simple Runner with Shiro security and JWT token support. Bypasses OSGi + * and wires security directly into the ActiveMQ broker. */ public class GossSimpleRunner { private BrokerService brokerService; + private Connection tokenHandlerConnection; + + // User database loaded from property file + private final Map userMap = new ConcurrentHashMap<>(); + private final Map> userPermissions = new ConcurrentHashMap<>(); + private final Map tokenCache = new ConcurrentHashMap<>(); + + // Security config for JWT token creation/validation + private SecurityConfigImpl securityConfig; + + private static final String SYSTEM_USER = "system"; + private static final String SYSTEM_PASSWORD = "manager"; + private static final String USER_PROPERTIES_FILENAME = "pnnl.goss.core.security.propertyfile.cfg"; + private static final String TOKEN_TOPIC = GossCoreContants.PROP_TOKEN_QUEUE; + + // Configurable ports (system property > env var > default) + private static final int DEFAULT_OPENWIRE_PORT = 61617; + private static final int DEFAULT_STOMP_PORT = 61618; + private static final int DEFAULT_WS_PORT = 61616; + + private int openwirePort; + private int stompPort; + private int wsPort; + + /** Read an int config value from system property, env var, or default. */ + private static int configInt(String sysProp, String envVar, int defaultVal) { + String val = System.getProperty(sysProp); + if (val != null && !val.isEmpty()) { + try { + return Integer.parseInt(val.trim()); + } catch (NumberFormatException e) { + System.err.println("Invalid integer value for system property '" + sysProp + + "': '" + val + "'. Falling back to environment variable or default."); + } + } + val = System.getenv(envVar); + if (val != null && !val.isEmpty()) { + try { + return Integer.parseInt(val.trim()); + } catch (NumberFormatException e) { + System.err.println("Invalid integer value for environment variable '" + envVar + + "': '" + val + "'. Falling back to default."); + } + } + return defaultVal; + } public static void main(String[] args) { System.out.println("Starting GOSS Simple Runner..."); @@ -41,28 +129,159 @@ public static void main(String[] args) { } public void start() throws Exception { - System.out.println("Starting ActiveMQ Broker..."); + // 0. Read configurable ports + openwirePort = configInt("goss.openwire.port", "GOSS_OPENWIRE_PORT", DEFAULT_OPENWIRE_PORT); + stompPort = configInt("goss.stomp.port", "GOSS_STOMP_PORT", DEFAULT_STOMP_PORT); + wsPort = configInt("goss.ws.port", "GOSS_WS_PORT", DEFAULT_WS_PORT); + + // 1. Load user properties + loadUserProperties(); + + // 2. Initialize SecurityConfig for JWT tokens + initSecurityConfig(); + + // 3. Start broker with Shiro security + System.out.println("Starting ActiveMQ Broker with Shiro security..."); startBroker(); - System.out.println("Security: Using default (no authentication)"); + // 4. Start token request handler + startTokenHandler(); System.out.println("GOSS Core services are running"); - System.out.println("ActiveMQ Broker: tcp://0.0.0.0:61617"); - System.out.println("STOMP: tcp://0.0.0.0:61618"); - System.out.println("WebSocket: disabled (to avoid Jetty dependencies)"); + System.out.println("ActiveMQ Broker: tcp://0.0.0.0:" + openwirePort); + System.out.println("STOMP: stomp://0.0.0.0:" + stompPort); + System.out.println("WebSocket: ws://0.0.0.0:" + wsPort); + System.out.println("Security: Shiro authentication enabled (" + userMap.size() + " users)"); + System.out.println("Token support: JWT token authentication enabled"); } public void stop() { try { + if (tokenHandlerConnection != null) { + tokenHandlerConnection.close(); + } if (brokerService != null) { brokerService.stop(); } - // No security manager to clean up } catch (Exception e) { System.err.println("Error stopping GOSS: " + e.getMessage()); } } + /** + * Locate the user properties file by checking (in order): 1. + * GOSS_USER_PROPERTIES env var / goss.user.properties system property 2. + * conf/ (next to the JAR / working dir) 3. + * pnnl.goss.core.runner/conf/ (run from GOSS root) 4. + * ../conf/ (JAR is inside generated/executable) 5. + * ../../conf/ + */ + private File findUserPropertiesFile() { + // Explicit override + String explicit = System.getProperty("goss.user.properties"); + if (explicit == null) + explicit = System.getenv("GOSS_USER_PROPERTIES"); + if (explicit != null) { + File f = new File(explicit); + if (f.exists()) + return f; + System.err.println("Explicit user properties path not found: " + explicit); + } + + // Resolve the directory that contains the JAR + File jarDir; + try { + jarDir = new File(GossSimpleRunner.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()).getParentFile(); + } catch (Exception e) { + jarDir = new File("."); + } + + String[] candidates = { + "conf/" + USER_PROPERTIES_FILENAME, + "pnnl.goss.core.runner/conf/" + USER_PROPERTIES_FILENAME, + jarDir + "/conf/" + USER_PROPERTIES_FILENAME, + jarDir + "/../conf/" + USER_PROPERTIES_FILENAME, + jarDir + "/../../conf/" + USER_PROPERTIES_FILENAME, + }; + + for (String path : candidates) { + File f = new File(path); + if (f.exists()) + return f; + } + return null; + } + + private void loadUserProperties() { + File propsFile = findUserPropertiesFile(); + if (propsFile == null) { + System.out.println("No user properties file (" + USER_PROPERTIES_FILENAME + + ") found in any search path"); + System.out.println("Using default system user only"); + SimpleAccount systemAcct = new SimpleAccount(SYSTEM_USER, SYSTEM_PASSWORD, "SimpleRunnerRealm"); + systemAcct.addStringPermission("*"); + userMap.put(SYSTEM_USER, systemAcct); + Set perms = new HashSet<>(); + perms.add("*"); + userPermissions.put(SYSTEM_USER, perms); + return; + } + + System.out.println("Loading users from " + propsFile.getAbsolutePath()); + try (BufferedReader reader = new BufferedReader(new FileReader(propsFile))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + int eqIdx = line.indexOf('='); + if (eqIdx < 0) { + continue; + } + String username = line.substring(0, eqIdx).trim(); + String value = line.substring(eqIdx + 1).trim(); + String[] parts = value.split(","); + if (parts.length < 1) { + continue; + } + + String password = parts[0]; + SimpleAccount acct = new SimpleAccount(username, password, "SimpleRunnerRealm"); + Set perms = new HashSet<>(); + for (int i = 1; i < parts.length; i++) { + String perm = parts[i].trim(); + if (!perm.isEmpty()) { + acct.addStringPermission(perm); + perms.add(perm); + } + } + userMap.put(username, acct); + userPermissions.put(username, perms); + } + System.out.println("Loaded " + userMap.size() + " users from " + propsFile.getAbsolutePath()); + } catch (Exception e) { + System.err.println("Error reading user properties: " + e.getMessage()); + // Fall back to default system user + SimpleAccount systemAcct = new SimpleAccount(SYSTEM_USER, SYSTEM_PASSWORD, "SimpleRunnerRealm"); + systemAcct.addStringPermission("*"); + userMap.put(SYSTEM_USER, systemAcct); + Set perms = new HashSet<>(); + perms.add("*"); + userPermissions.put(SYSTEM_USER, perms); + } + } + + private void initSecurityConfig() { + // Create SecurityConfigImpl with system manager credentials + securityConfig = new SecurityConfigImpl(); + Map secProps = new HashMap<>(); + secProps.put("goss.system.manager", SYSTEM_USER); + secProps.put("goss.system.manager.password", SYSTEM_PASSWORD); + securityConfig.updated(secProps); + } + private void startBroker() throws Exception { brokerService = new BrokerService(); brokerService.setBrokerName("goss-broker"); @@ -73,20 +292,251 @@ private void startBroker() throws Exception { systemUsage.getMemoryUsage().setLimit(64 * 1024 * 1024); // 64MB systemUsage.getStoreUsage().setLimit(1024 * 1024 * 1024); // 1GB - // Add connectors with different ports + // Set up Shiro security + DefaultActiveMqSecurityManager securityManager = new DefaultActiveMqSecurityManager(); + securityManager.setCacheManager(new MemoryConstrainedCacheManager()); + + // Create realms + Set realms = new HashSet<>(); + + // Property-based realm for username/password auth + PropertyRealm propertyRealm = new PropertyRealm(); + realms.add(propertyRealm); + + // Token-based realm for JWT auth + TokenRealm tokenRealm = new TokenRealm(); + realms.add(tokenRealm); + + securityManager.setRealms(realms); + SecurityUtils.setSecurityManager(securityManager); + + // Attach ShiroPlugin to broker + ShiroPlugin shiroPlugin = new ShiroPlugin(); + shiroPlugin.setSecurityManager(securityManager); + brokerService.setPlugins(new BrokerPlugin[]{shiroPlugin}); + + // Add connectors TransportConnector openwireConnector = new TransportConnector(); - openwireConnector.setUri(new URI("tcp://0.0.0.0:61617")); + openwireConnector.setUri(new URI("tcp://0.0.0.0:" + openwirePort)); openwireConnector.setName("openwire"); brokerService.addConnector(openwireConnector); TransportConnector stompConnector = new TransportConnector(); - stompConnector.setUri(new URI("stomp://0.0.0.0:61618")); + stompConnector.setUri(new URI("stomp://0.0.0.0:" + stompPort)); stompConnector.setName("stomp"); brokerService.addConnector(stompConnector); - // WebSocket connector removed - requires Jetty dependencies + TransportConnector wsConnector = new TransportConnector(); + wsConnector.setUri(new URI("ws://0.0.0.0:" + wsPort + + "?websocket.maxTextMessageSize=999999" + + "&websocket.maxIdleTime=60000" + + "&websocket.bufferSize=32536")); + wsConnector.setName("ws"); + brokerService.addConnector(wsConnector); brokerService.start(); } + private void startTokenHandler() throws Exception { + // Connect to the local broker as the system user + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:" + openwirePort); + factory.setUserName(SYSTEM_USER); + factory.setPassword(SYSTEM_PASSWORD); + tokenHandlerConnection = factory.createConnection(); + tokenHandlerConnection.start(); + + Session session = tokenHandlerConnection.createSession(false, Session.AUTO_ACKNOWLEDGE); + Topic tokenTopic = session.createTopic(TOKEN_TOPIC); + MessageConsumer consumer = session.createConsumer(tokenTopic); + + // Also create a producer for sending responses + MessageProducer producer = session.createProducer(null); // null dest = use per-message dest + + consumer.setMessageListener(new MessageListener() { + @Override + public void onMessage(Message message) { + try { + String body = null; + if (message instanceof TextMessage) { + body = ((TextMessage) message).getText(); + } else if (message instanceof BytesMessage) { + BytesMessage bm = (BytesMessage) message; + byte[] bytes = new byte[(int) bm.getBodyLength()]; + bm.readBytes(bytes); + body = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + } + + if (body == null || body.isEmpty()) { + return; + } + + // Decode base64(username:password) + String decoded = new String(Base64.getDecoder().decode(body.trim())); + String[] authParts = decoded.split(":", 2); + if (authParts.length < 2) { + return; + } + + String userId = authParts[0]; + String password = authParts[1]; + String responseData; + + // Validate credentials + SimpleAccount acct = userMap.get(userId); + if (acct != null && password.equals(acct.getCredentials())) { + // Create or reuse cached token + String token = tokenCache.get(userId); + if (token == null) { + token = securityConfig.createToken(userId, + userPermissions.getOrDefault(userId, new HashSet<>())); + tokenCache.put(userId, token); + System.out.println("Created token for user: " + userId); + } + responseData = token; + } else { + System.out.println("Authentication failed for user: " + userId); + responseData = "authentication failed"; + } + + // Send response to reply-to destination + Destination replyTo = message.getJMSReplyTo(); + System.out.println("Token handler: JMSReplyTo=" + replyTo); + if (replyTo != null) { + TextMessage response = session.createTextMessage(responseData); + producer.send(replyTo, response); + System.out.println("Token response sent to: " + replyTo); + } else { + // STOMP reply-to may be stored as a string property + String replyToStr = message.getStringProperty("reply-to"); + System.out.println("Token handler: reply-to property=" + replyToStr); + if (replyToStr != null && !replyToStr.isEmpty()) { + Destination replyDest; + if (replyToStr.startsWith("/queue/")) { + replyDest = session.createQueue(replyToStr.substring("/queue/".length())); + } else if (replyToStr.startsWith("/topic/")) { + replyDest = session.createTopic(replyToStr.substring("/topic/".length())); + } else { + // Default to queue + replyDest = session.createQueue(replyToStr); + } + TextMessage response = session.createTextMessage(responseData); + producer.send(replyDest, response); + System.out.println("Token response sent to fallback dest: " + replyDest); + } else { + System.err.println("No reply-to destination found on token request message"); + } + } + } catch (Exception e) { + System.err.println("Error handling token request: " + e.getMessage()); + e.printStackTrace(); + } + } + }); + + System.out.println("Token handler listening on /topic/" + TOKEN_TOPIC); + } + + /** + * Simple realm for username/password authentication from the property file. + */ + private class PropertyRealm extends AuthorizingRealm { + + private final GossWildcardPermissionResolver resolver = new GossWildcardPermissionResolver(); + + PropertyRealm() { + setName("PropertyRealm"); + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + String username = (String) getAvailablePrincipal(principals); + return userMap.get(username); + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + UsernamePasswordToken upToken = (UsernamePasswordToken) token; + String username = upToken.getUsername(); + if (username == null) { + return null; + } + // Don't handle tokens (long username, empty password) — let TokenRealm do that + char[] pw = upToken.getPassword(); + if (username.length() > 250 && (pw == null || pw.length == 0)) { + return null; + } + return userMap.get(username); + } + + @Override + public PermissionResolver getPermissionResolver() { + return resolver; + } + } + + /** + * Realm for JWT token-based authentication. Detects tokens by: username > 250 + * chars and empty password. + */ + private class TokenRealm extends AuthorizingRealm { + + private final Map tokenAccountMap = new ConcurrentHashMap<>(); + private final GossWildcardPermissionResolver resolver = new GossWildcardPermissionResolver(); + + TokenRealm() { + setName("TokenRealm"); + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + String username = (String) getAvailablePrincipal(principals); + return tokenAccountMap.get(username); + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + UsernamePasswordToken upToken = (UsernamePasswordToken) token; + String username = upToken.getUsername(); + char[] pw = upToken.getPassword(); + + // Only handle JWT tokens: long username, empty password + if (username == null || username.length() <= 250 || (pw != null && pw.length > 0)) { + return null; + } + + // Validate the JWT token + boolean verified = securityConfig.validateToken(username); + if (!verified) { + return null; + } + + // Parse token to extract roles/permissions + JWTAuthenticationToken jwtToken = securityConfig.parseToken(username); + if (jwtToken == null) { + return null; + } + + SimpleAccount acct = new SimpleAccount(username, "", getName()); + // Grant the permissions from the user's roles + if (jwtToken.getRoles() != null) { + for (String perm : jwtToken.getRoles()) { + acct.addStringPermission(perm); + } + } + // Also grant wildcard for token-authenticated users + // (matching the test expectations for pub/sub) + acct.addStringPermission("topic:*"); + acct.addStringPermission("queue:*"); + acct.addStringPermission("temp-queue:*"); + tokenAccountMap.put(username, acct); + return acct; + } + + @Override + public PermissionResolver getPermissionResolver() { + return resolver; + } + } } diff --git a/pnnl.goss.core.testutil/.gitignore b/pnnl.goss.core.testutil/.gitignore index 90dde36e..61735c10 100644 --- a/pnnl.goss.core.testutil/.gitignore +++ b/pnnl.goss.core.testutil/.gitignore @@ -1,3 +1 @@ -/bin/ -/bin_test/ -/generated/ +# Covered by root .gitignore diff --git a/pnnl.goss.core.testutil/bnd.bnd b/pnnl.goss.core.testutil/bnd.bnd index 7eb80bb0..8d42825a 100644 --- a/pnnl.goss.core.testutil/bnd.bnd +++ b/pnnl.goss.core.testutil/bnd.bnd @@ -1,4 +1,4 @@ -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 -buildpath: \ ${configadmin-buildpath},\ pnnl.goss.core.core-api,\ diff --git a/pnnl.goss.core/.gitignore b/pnnl.goss.core/.gitignore index 7fdbdef7..61735c10 100644 --- a/pnnl.goss.core/.gitignore +++ b/pnnl.goss.core/.gitignore @@ -1,2 +1 @@ -/bin/ -/bin_test/ +# Covered by root .gitignore diff --git a/pnnl.goss.core/bnd.bnd b/pnnl.goss.core/bnd.bnd index b677e69a..a51e7ce7 100644 --- a/pnnl.goss.core/bnd.bnd +++ b/pnnl.goss.core/bnd.bnd @@ -29,6 +29,8 @@ org.springframework:spring-core;version=6.2.0,\ javax.annotation:javax.annotation-api;version=1.3.2,\ com.thoughtworks.xstream:xstream;version=1.4.20,\ + com.nimbusds:nimbus-jose-jwt;version=9.31,\ + net.minidev:json-smart;version=2.4.11,\ junit:junit;version=4.13.2 # -plugin org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin;log=debug diff --git a/pnnl.goss.core/core-api.bnd b/pnnl.goss.core/core-api.bnd index 40a19a3b..7747746e 100644 --- a/pnnl.goss.core/core-api.bnd +++ b/pnnl.goss.core/core-api.bnd @@ -1,4 +1,4 @@ Export-Package: \ com.northconcepts.exception,\ pnnl.goss.core -Bundle-Version: 12.1.0 \ No newline at end of file +Bundle-Version: 15.0.2 \ No newline at end of file diff --git a/pnnl.goss.core/goss-client.bnd b/pnnl.goss.core/goss-client.bnd index 993f7e01..d10734a0 100644 --- a/pnnl.goss.core/goss-client.bnd +++ b/pnnl.goss.core/goss-client.bnd @@ -1,3 +1,3 @@ Export-Package: \ pnnl.goss.core.client -Bundle-Version: 12.1.0 \ No newline at end of file +Bundle-Version: 15.0.2 \ No newline at end of file diff --git a/pnnl.goss.core/goss-core-commands.bnd b/pnnl.goss.core/goss-core-commands.bnd index fb119956..e5238b0b 100644 --- a/pnnl.goss.core/goss-core-commands.bnd +++ b/pnnl.goss.core/goss-core-commands.bnd @@ -1,3 +1,3 @@ Private-Package: \ pnnl.goss.core.commands -Bundle-Version: 12.1.0 \ No newline at end of file +Bundle-Version: 15.0.2 \ No newline at end of file diff --git a/pnnl.goss.core/goss-core-exceptions.bnd b/pnnl.goss.core/goss-core-exceptions.bnd index 671b7944..b480c3ef 100644 --- a/pnnl.goss.core/goss-core-exceptions.bnd +++ b/pnnl.goss.core/goss-core-exceptions.bnd @@ -1,5 +1,5 @@ Private-Package: \ pnnl.goss.core.exception -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 Export-Package: \ com.northconcepts.exception \ No newline at end of file diff --git a/pnnl.goss.core/goss-core-security.bnd b/pnnl.goss.core/goss-core-security.bnd index 25c308dc..060f505f 100644 --- a/pnnl.goss.core/goss-core-security.bnd +++ b/pnnl.goss.core/goss-core-security.bnd @@ -5,7 +5,7 @@ Private-Package: \ # The Activator class provides SecurityManager via @Component annotation Export-Package: \ pnnl.goss.core.security -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 # Require FileInstall to be present in the runtime # FileInstall watches the conf directory for .cfg files and loads them into ConfigAdmin diff --git a/pnnl.goss.core/goss-core-server-api.bnd b/pnnl.goss.core/goss-core-server-api.bnd index bd947e07..10134142 100644 --- a/pnnl.goss.core/goss-core-server-api.bnd +++ b/pnnl.goss.core/goss-core-server-api.bnd @@ -1,3 +1,3 @@ Export-Package: \ pnnl.goss.core.server -Bundle-Version: 12.1.0 \ No newline at end of file +Bundle-Version: 15.0.2 \ No newline at end of file diff --git a/pnnl.goss.core/goss-core-server-registry.bnd b/pnnl.goss.core/goss-core-server-registry.bnd index c077b74a..e3c39196 100644 --- a/pnnl.goss.core/goss-core-server-registry.bnd +++ b/pnnl.goss.core/goss-core-server-registry.bnd @@ -1,4 +1,4 @@ -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 Private-Package: \ pnnl.goss.server.registry DynamicImport-Package: * \ No newline at end of file diff --git a/pnnl.goss.core/goss-core-server-web.bnd b/pnnl.goss.core/goss-core-server-web.bnd index 0c3b237c..d346a46c 100644 --- a/pnnl.goss.core/goss-core-server-web.bnd +++ b/pnnl.goss.core/goss-core-server-web.bnd @@ -2,7 +2,7 @@ DynamicImport-Package: * Private-Package: \ pnnl.goss.core.server.web -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 # Import webroot folder to path resources/webroot Include-Resource: resources/webroot=webroot diff --git a/pnnl.goss.core/goss-core-server.bnd b/pnnl.goss.core/goss-core-server.bnd index ecd0fffe..137fc0df 100644 --- a/pnnl.goss.core/goss-core-server.bnd +++ b/pnnl.goss.core/goss-core-server.bnd @@ -9,4 +9,4 @@ Import-Package: \ * #Include-Resource: \ # OSGI-INF/blueprint/blueprint.xml=config/blueprint.xml -Bundle-Version: 12.1.0 \ No newline at end of file +Bundle-Version: 15.0.2 \ No newline at end of file diff --git a/pnnl.goss.core/security-jwt.bnd b/pnnl.goss.core/security-jwt.bnd index 1ec5a72d..6f66b18e 100644 --- a/pnnl.goss.core/security-jwt.bnd +++ b/pnnl.goss.core/security-jwt.bnd @@ -1,2 +1,2 @@ Private-Package: pnnl.goss.core.security.jwt -Bundle-Version: 1.0.150.${tstamp} +Bundle-Version: 15.0.2 diff --git a/pnnl.goss.core/security-ldap.bnd b/pnnl.goss.core/security-ldap.bnd index 21388870..dc6cb267 100644 --- a/pnnl.goss.core/security-ldap.bnd +++ b/pnnl.goss.core/security-ldap.bnd @@ -1,3 +1,3 @@ Private-Package: \ pnnl.goss.core.security.ldap -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 diff --git a/pnnl.goss.core/security-propertyfile.bnd b/pnnl.goss.core/security-propertyfile.bnd index b21e4f26..ca923f7e 100644 --- a/pnnl.goss.core/security-propertyfile.bnd +++ b/pnnl.goss.core/security-propertyfile.bnd @@ -1,3 +1,3 @@ Private-Package: \ pnnl.goss.core.security.propertyfile -Bundle-Version: 12.1.0 +Bundle-Version: 15.0.2 diff --git a/pnnl.goss.core/security-system.bnd b/pnnl.goss.core/security-system.bnd index 1294709d..d82b28f1 100644 --- a/pnnl.goss.core/security-system.bnd +++ b/pnnl.goss.core/security-system.bnd @@ -1,2 +1,2 @@ Private-Package: pnnl.goss.core.security.system -Bundle-Version: 2.0.145.${tstamp} +Bundle-Version: 15.0.2 diff --git a/pnnl.goss.core/src/pnnl/goss/core/GossCoreContants.java b/pnnl.goss.core/src/pnnl/goss/core/GossCoreContants.java index 222e6a78..6a2bc22a 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/GossCoreContants.java +++ b/pnnl.goss.core/src/pnnl/goss/core/GossCoreContants.java @@ -36,4 +36,6 @@ public class GossCoreContants { public static final String PROP_REQUEST_QUEUE = "pnnl.goss.request.topic"; public static final String PROP_TICK_TOPIC = "pnnl.goss.tick.topic"; + + public static final String PROP_TOKEN_QUEUE = "pnnl.goss.token.topic"; } diff --git a/pnnl.goss.core/src/pnnl/goss/core/client/DefaultClientListener.java b/pnnl.goss.core/src/pnnl/goss/core/client/DefaultClientListener.java index e74ce244..b38cfdfc 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/client/DefaultClientListener.java +++ b/pnnl.goss.core/src/pnnl/goss/core/client/DefaultClientListener.java @@ -1,10 +1,14 @@ package pnnl.goss.core.client; import jakarta.jms.BytesMessage; +import jakarta.jms.Destination; import jakarta.jms.Message; import jakarta.jms.ObjectMessage; import jakarta.jms.TextMessage; +import org.apache.activemq.command.ActiveMQDestination; +import org.apache.activemq.command.ActiveMQQueue; +import org.apache.activemq.command.ActiveMQTopic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,6 +28,36 @@ public DefaultClientListener(GossResponseEvent event) { responseEvent = event; } + /** + * Get the reply destination from a JMS message, falling back to string + * properties when the STOMP adapter can't parse a bare reply-to header into a + * JMS Destination (e.g., "feeder-models" instead of "/queue/feeder-models"). + */ + private Destination getReplyDestination(Message msg) { + try { + Destination dest = msg.getJMSReplyTo(); + if (dest != null) { + // Accept any ActiveMQ destination type directly, including + // ActiveMQTempQueue and ActiveMQTempTopic which do NOT extend + // ActiveMQQueue/ActiveMQTopic (they extend ActiveMQTempDestination). + if (dest instanceof ActiveMQDestination) + return dest; + log.debug("Normalizing reply destination: {} -> ActiveMQQueue", dest); + return new ActiveMQQueue(dest.toString()); + } + // JMSReplyTo is null - check for bare reply-to string property + // ActiveMQ STOMP adapter sets this when it can't map to a Destination + String replyTo = msg.getStringProperty("reply-to"); + if (replyTo != null && !replyTo.isEmpty()) { + log.debug("Using bare reply-to property as queue: {}", replyTo); + return new ActiveMQQueue(replyTo); + } + } catch (Exception e) { + log.warn("Failed to get reply destination", e); + } + return null; + } + public void onMessage(Message message) { log.info("DefaultClientListener.onMessage called with message type: {}", message != null ? message.getClass().getSimpleName() : "null"); @@ -40,8 +74,9 @@ public void onMessage(Message message) { if (response.getDestination() == null) response.setDestination(message.getJMSDestination().toString()); // Set reply destination and username from JMS headers - if (message.getJMSReplyTo() != null) - response.setReplyDestination(message.getJMSReplyTo()); + Destination replyDest = getReplyDestination(message); + if (replyDest != null) + response.setReplyDestination(replyDest); if (message.getStringProperty(SecurityConstants.SUBJECT_HEADER) != null) response.setUsername(message.getStringProperty(SecurityConstants.SUBJECT_HEADER)); responseEvent.onMessage(response); @@ -52,8 +87,9 @@ public void onMessage(Message message) { if (response.getDestination() == null) response.setDestination(message.getJMSDestination().toString()); // Set reply destination and username from JMS headers - if (message.getJMSReplyTo() != null) - response.setReplyDestination(message.getJMSReplyTo()); + Destination replyDest = getReplyDestination(message); + if (replyDest != null) + response.setReplyDestination(replyDest); if (message.getStringProperty(SecurityConstants.SUBJECT_HEADER) != null) response.setUsername(message.getStringProperty(SecurityConstants.SUBJECT_HEADER)); responseEvent.onMessage(response); @@ -71,8 +107,9 @@ public void onMessage(Message message) { if (response.getDestination() == null) response.setDestination(message.getJMSDestination().toString()); // Set reply destination and username from JMS headers - if (message.getJMSReplyTo() != null) - response.setReplyDestination(message.getJMSReplyTo()); + Destination replyDest = getReplyDestination(message); + if (replyDest != null) + response.setReplyDestination(replyDest); if (message.getStringProperty(SecurityConstants.SUBJECT_HEADER) != null) response.setUsername(message.getStringProperty(SecurityConstants.SUBJECT_HEADER)); responseEvent.onMessage(response); diff --git a/pnnl.goss.core/src/pnnl/goss/core/client/GossClient.java b/pnnl.goss.core/src/pnnl/goss/core/client/GossClient.java index f85959bd..3bc1046e 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/client/GossClient.java +++ b/pnnl.goss.core/src/pnnl/goss/core/client/GossClient.java @@ -673,6 +673,19 @@ private Destination getDestination(String destinationName, DESTINATION_TYPE dest try { if (protocol.equals(PROTOCOL.OPENWIRE) || protocol.equals(PROTOCOL.STOMP)) { + // Strip STOMP-style prefixes from destination names. + // STOMP protocol uses /topic/ and /queue/ prefixes to identify destination + // type, but JMS uses the raw name. Since this client uses JMS/OpenWire + // internally (even for PROTOCOL.STOMP), we need to strip the prefixes + // so JMS topic names match what STOMP clients (browser, Python) send to. + if (destinationName.startsWith("/topic/")) { + destinationName = destinationName.substring(7); + destinationType = DESTINATION_TYPE.TOPIC; + } else if (destinationName.startsWith("/queue/")) { + destinationName = destinationName.substring(7); + destinationType = DESTINATION_TYPE.QUEUE; + } + // Both OPENWIRE and STOMP use standard JMS with ActiveMQ if (destinationType == DESTINATION_TYPE.QUEUE) { destination = getSession().createQueue(destinationName); diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/impl/MACVerifierExtended.java b/pnnl.goss.core/src/pnnl/goss/core/security/impl/MACVerifierExtended.java index fa15a16a..b3b81736 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/impl/MACVerifierExtended.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/impl/MACVerifierExtended.java @@ -23,17 +23,17 @@ public MACVerifierExtended(final String sharedSecretString, JWTClaimsSet claimsS } @Override - public boolean verify(final JWSHeader header, final byte[] signingInput, final Base64URL signature) throws JOSEException { + public boolean verify(final JWSHeader header, final byte[] signingInput, final Base64URL signature) + throws JOSEException { boolean value = super.verify(header, signingInput, signature); -// long time = System.currentTimeMillis(); - + // long time = System.currentTimeMillis(); + long time = new Date().getTime(); - time=time*1000; - + time = time * 1000; + boolean notBeforeTime = claimsSet.getNotBeforeTime().getTime() <= time; boolean beforeExpiration = time < claimsSet.getExpirationTime().getTime(); - - + return value && notBeforeTime && beforeExpiration; } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/impl/RoleManagerImpl.java b/pnnl.goss.core/src/pnnl/goss/core/security/impl/RoleManagerImpl.java index bf9e3217..872d3336 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/impl/RoleManagerImpl.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/impl/RoleManagerImpl.java @@ -1,123 +1,93 @@ package pnnl.goss.core.security.impl; -import java.util.Dictionary; -import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.apache.felix.dm.annotation.api.Component; -import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Modified; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.northconcepts.exception.SystemException; - import pnnl.goss.core.security.RoleManager; - - -@Component +@Component(service = RoleManager.class, configurationPid = "pnnl.goss.core.security.rolefile", configurationPolicy = ConfigurationPolicy.REQUIRE) public class RoleManagerImpl implements RoleManager { - private static final Logger log = LoggerFactory.getLogger(RoleManagerImpl.class); - private static final String CONFIG_PID = "pnnl.goss.core.security.rolefile"; - -// private HashMap> roles = new HashMap>(); - private final Map> rolePermissions = new ConcurrentHashMap<>(); - - -// -// -// @Override -// public List getRoles(String userName) throws Exception { -// if(!roles.containsKey(userName)){ -// throw new Exception("No roles specified for user "+userName); -// } -// -// return roles.get(userName); -// } -// -// @Override -// public boolean hasRole(String userName, String roleName) throws Exception { -// if(!roles.containsKey(userName)){ -// throw new Exception("No roles specified for user "+userName); -// } -// -// List groups = roles.get(userName); -// return groups.contains(roleName); -// } - - - - @ConfigurationDependency(pid=CONFIG_PID) - public synchronized void updated(Dictionary properties) throws SystemException { - if (properties != null){ - log.debug("Updating RoleManagerImpl"); - rolePermissions.clear(); - - Enumeration keys = properties.keys(); - Set perms = new HashSet<>(); - while(keys.hasMoreElements()){ - String k = keys.nextElement(); - String v = (String)properties.get(k); - String[] credAndPermissions = v.split(","); - - for(int i =0; i keys = properties.keys(); -// -// while(keys.hasMoreElements()){ -// String user = keys.nextElement(); -// String groups = properties.get(user).toString(); -// System.out.println("Registering user roles: "+user+" -- "+groups); -// List groupList = new ArrayList<>(Arrays.asList(StringUtils.split(groups, ','))); -// roles.put(user, groupList); -// } -// } - } - - - @Override - public Set getRolePermissions(String roleName) throws Exception { - if(rolePermissions.containsKey(roleName)){ - return rolePermissions.get(roleName); - } else { - return null; - } - } - - - @Override - public Set getAllRoles() { - return rolePermissions.keySet(); - } - - - @Override - public Set getRolePermissions(List roleNames) throws Exception { - Set perms = new HashSet<>(); - for(String role: roleNames){ - Set rolePerms = rolePermissions.get(role); - for (String p: rolePerms){ - if(!perms.contains(p)){ - perms.add(p); - } - } - } - - return perms; - } + private static final Logger log = LoggerFactory.getLogger(RoleManagerImpl.class); + + private final Map> rolePermissions = new ConcurrentHashMap<>(); + + @Activate + public void activate(Map properties) { + log.info("Activating RoleManagerImpl"); + updated(properties); + } + + @Modified + public synchronized void updated(Map properties) { + if (properties != null) { + log.debug("Updating RoleManagerImpl"); + rolePermissions.clear(); + + for (String k : properties.keySet()) { + // Skip OSGi/ConfigAdmin metadata properties + if (k.startsWith("service.") || k.startsWith("component.") || + k.startsWith("felix.") || k.equals("osgi.ds.satisfying.condition.target")) { + continue; + } + + Object value = properties.get(k); + if (!(value instanceof String)) { + continue; + } + + String v = (String) value; + String[] credAndPermissions = v.split(","); + Set perms = new HashSet<>(); + + for (int i = 0; i < credAndPermissions.length; i++) { + perms.add(credAndPermissions[i]); + } + rolePermissions.put(k, perms); + } + log.info("RoleManagerImpl configured with {} roles", rolePermissions.size()); + } + } + + @Override + public Set getRolePermissions(String roleName) throws Exception { + if (rolePermissions.containsKey(roleName)) { + return rolePermissions.get(roleName); + } else { + return null; + } + } + + @Override + public Set getAllRoles() { + return rolePermissions.keySet(); + } + + @Override + public Set getRolePermissions(List roleNames) throws Exception { + Set perms = new HashSet<>(); + for (String role : roleNames) { + Set rolePerms = rolePermissions.get(role); + if (rolePerms != null) { + for (String p : rolePerms) { + if (!perms.contains(p)) { + perms.add(p); + } + } + } + } + + return perms; + } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityConfigImpl.java b/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityConfigImpl.java index 7162c3b4..a9acd8ca 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityConfigImpl.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityConfigImpl.java @@ -4,11 +4,14 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Date; -import java.util.Dictionary; +import java.util.Map; import java.util.Set; import java.util.UUID; -import org.apache.felix.dm.annotation.api.Component; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Modified; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,123 +31,118 @@ import pnnl.goss.core.security.SecurityConfig; import pnnl.goss.core.security.SecurityConstants; - -@Component +@Component(service = SecurityConfig.class, configurationPid = "pnnl.goss.security", configurationPolicy = ConfigurationPolicy.REQUIRE) public class SecurityConfigImpl implements SecurityConfig { - private String managerUser; - private String managerPassword; - private boolean useToken = false; - private byte[] sharedKey = generateSharedKey(); - - private Dictionary properties; - private static final Logger log = LoggerFactory.getLogger(SecurityConfigImpl.class); - private static final String ISSUED_BY = "GridOPTICS Software System"; - - - - - public SecurityConfigImpl(){ - } - - - protected long getExpirationDate() { + private String managerUser; + private String managerPassword; + private boolean useToken = false; + private byte[] sharedKey = generateSharedKey(); + + private Map properties; + private static final Logger log = LoggerFactory.getLogger(SecurityConfigImpl.class); + private static final String ISSUED_BY = "GridOPTICS Software System"; + + public SecurityConfigImpl() { + } + + protected long getExpirationDate() { return 1000 * 60 * 60 * 24 * 5; } - protected String getIssuer(){return ISSUED_BY;} + protected String getIssuer() { + return ISSUED_BY; + } + + @Activate + public void activate(Map properties) { + log.info("Activating SecurityConfigImpl"); + updated(properties); + } - - void updated(Dictionary properties) { + @Modified + public synchronized void updated(Map properties) { if (properties != null) { - this.properties = properties; - - // create system realm - managerUser = getProperty(SecurityConstants.PROP_SYSTEM_MANAGER - ,null); - managerPassword = getProperty(SecurityConstants.PROP_SYSTEM_MANAGER_PASSWORD - ,null); - - String secret = getProperty(SecurityConstants.PROP_SYSTEM_TOKEN_SECRET, null); - if(secret!=null && secret.trim().length()>0){ - this.sharedKey = secret.getBytes(); - } - - String useTokenString = getProperty(SecurityConstants.PROP_SYSTEM_USE_TOKEN - ,null); - if(secret!=null && secret.trim().length()>0){ - try{ - this.useToken = new Boolean(useTokenString); - }catch (Exception e) { - log.error("Could not parse use token parameter as boolean in security config: '"+useTokenString+"'"); - } - } - - + this.properties = properties; + + // create system realm + managerUser = getProperty(SecurityConstants.PROP_SYSTEM_MANAGER, null); + managerPassword = getProperty(SecurityConstants.PROP_SYSTEM_MANAGER_PASSWORD, null); + + String secret = getProperty(SecurityConstants.PROP_SYSTEM_TOKEN_SECRET, null); + if (secret != null && secret.trim().length() > 0) { + this.sharedKey = secret.getBytes(); + } + + String useTokenString = getProperty(SecurityConstants.PROP_SYSTEM_USE_TOKEN, null); + if (useTokenString != null && useTokenString.trim().length() > 0) { + try { + this.useToken = Boolean.parseBoolean(useTokenString); + } catch (Exception e) { + log.error("Could not parse use token parameter as boolean in security config: '" + useTokenString + + "'"); + } + } + } else { - log.error("No core config properties received by security activator"); - throw new SystemException("No security config properties received by activator", null); + log.error("No core config properties received by security activator"); + throw new SystemException("No security config properties received by activator", null); + } + } + + public String getProperty(String key, String defaultValue) { + String retValue = defaultValue; + + if (key != null && !key.isEmpty() && properties.get(key) != null) { + Object value = properties.get(key); + if (value instanceof String) { + String strValue = (String) value; + // Let the value pass through because it doesn't + // start with ${ + if (!strValue.startsWith("${")) { + retValue = strValue; + } + } } + + return retValue; + } + + @Override + public String getManagerUser() { + return managerUser; + } + + @Override + public String getManagerPassword() { + return managerPassword; + } + + @Override + public boolean getUseToken() { + return useToken; } - - - - public String getProperty(String key, String defaultValue){ - String retValue = defaultValue; - - if (key != null && !key.isEmpty() && properties.get(key)!=null){ - String value = properties.get(key).toString(); - // Let the value pass through because it doesn't - // start with ${ - if (!value.startsWith("${")){ - retValue = value; - } - } - - return retValue; - } - - - @Override - public String getManagerUser() { - return managerUser; - } - - @Override - public String getManagerPassword() { - return managerPassword; - } - - - - @Override - public boolean getUseToken() { - // TODO Auto-generated method stub - return false; - } - - - + private byte[] generateSharedKey() { SecureRandom random = new SecureRandom(); byte[] sharedKey = new byte[32]; random.nextBytes(sharedKey); return sharedKey; } - - private byte[] getSharedKey(){ - if (sharedKey==null ) - sharedKey = generateSharedKey(); - return sharedKey; - } - + + private byte[] getSharedKey() { + if (sharedKey == null) + sharedKey = generateSharedKey(); + return sharedKey; + } + public boolean validateToken(String token) { - log.debug("Validate token "+token); + log.debug("Validating token"); try { SignedJWT signed = SignedJWT.parse(token); JWSVerifier verifier = new MACVerifierExtended(getSharedKey(), signed.getJWTClaimsSet()); boolean verified = signed.verify(verifier); - log.debug("Verified: "+verified); + log.debug("Verified: " + verified); return verified; } catch (ParseException ex) { return false; @@ -153,12 +151,12 @@ public boolean validateToken(String token) { } } - - public String createToken(Object userId, Set roles) { - log.info("Creating token for user "+userId); + + public String createToken(Object userId, Set roles) { + log.info("Creating token for user " + userId); try { - //TODO, should also include roles(permissions) - + // TODO, should also include roles(permissions) + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); builder.issuer(getIssuer()); builder.subject(userId.toString()); @@ -166,7 +164,7 @@ public String createToken(Object userId, Set roles) { builder.notBeforeTime(new Date()); builder.expirationTime(new Date(new Date().getTime() + getExpirationDate())); builder.jwtID(UUID.randomUUID().toString()); - + JWTAuthenticationToken tokenObj = new JWTAuthenticationToken(); tokenObj.setIss(getIssuer()); tokenObj.setSub(userId.toString()); @@ -177,7 +175,7 @@ public String createToken(Object userId, Set roles) { tokenObj.setRoles(new ArrayList(roles)); Payload payload = new Payload(tokenObj.toString()); -// JWTClaimsSet claimsSet = builder.build(); + // JWTClaimsSet claimsSet = builder.build(); JWSHeader header = new JWSHeader(JWSAlgorithm.HS256); JWSObject jwsObject = new JWSObject(header, payload); @@ -188,19 +186,19 @@ public String createToken(Object userId, Set roles) { return null; } } - - public JWTAuthenticationToken parseToken(String token){ - try{ - SignedJWT signed = SignedJWT.parse(token); - Payload payload = signed.getPayload(); - String jsonToken = payload.toJSONObject().toJSONString(); - log.debug("Json token: "+jsonToken); - // look up permissions based on roles and add them - JWTAuthenticationToken tokenObj = JWTAuthenticationToken.parse(jsonToken); - return tokenObj; - }catch (ParseException e) { - // TODO: handle exception - return null; - } + + public JWTAuthenticationToken parseToken(String token) { + try { + SignedJWT signed = SignedJWT.parse(token); + Payload payload = signed.getPayload(); + String jsonToken = payload.toString(); + log.debug("Json token: " + jsonToken); + // look up permissions based on roles and add them + JWTAuthenticationToken tokenObj = JWTAuthenticationToken.parse(jsonToken); + return tokenObj; + } catch (ParseException e) { + // TODO: handle exception + return null; + } } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityManagerImpl.java b/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityManagerImpl.java index 921352b6..b7761374 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityManagerImpl.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/impl/SecurityManagerImpl.java @@ -14,57 +14,53 @@ import pnnl.goss.core.security.GossSecurityManager; -public class SecurityManagerImpl extends DefaultActiveMqSecurityManager implements GossSecurityManager{ - private static final Logger log = LoggerFactory.getLogger(SecurityManagerImpl.class); +public class SecurityManagerImpl extends DefaultActiveMqSecurityManager implements GossSecurityManager { + private static final Logger log = LoggerFactory.getLogger(SecurityManagerImpl.class); - private Dictionary properties; - - - void updated(Dictionary properties) { + private Dictionary properties; + + void updated(Dictionary properties) { if (properties != null) { - this.properties = properties; - - // create system realm - String systemManager = getProperty(PROP_SYSTEM_MANAGER - ,null); - String systemManagerPassword = getProperty(PROP_SYSTEM_MANAGER_PASSWORD - ,null); - - Realm defaultRealm; - try { - defaultRealm = new SystemRealm(systemManager, systemManagerPassword); - - Set realms = new HashSet(); - realms.add(defaultRealm); - - setRealms(realms); - - SecurityUtils.setSecurityManager(this); - - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + this.properties = properties; + + // create system realm + String systemManager = getProperty(PROP_SYSTEM_MANAGER, null); + String systemManagerPassword = getProperty(PROP_SYSTEM_MANAGER_PASSWORD, null); + + Realm defaultRealm; + try { + defaultRealm = new SystemRealm(systemManager, systemManagerPassword); + + Set realms = new HashSet(); + realms.add(defaultRealm); + + setRealms(realms); + + SecurityUtils.setSecurityManager(this); + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } else { - log.error("No core config properties received by security activator"); - throw new SystemException("No security config properties received by activator", null); + log.error("No core config properties received by security activator"); + throw new SystemException("No security config properties received by activator", null); } } - - - public String getProperty(String key, String defaultValue){ - String retValue = defaultValue; - - if (key != null && !key.isEmpty() && properties.get(key)!=null){ - String value = properties.get(key).toString(); - // Let the value pass through because it doesn't - // start with ${ - if (!value.startsWith("${")){ - retValue = value; - } - } - - return retValue; - } - + + public String getProperty(String key, String defaultValue) { + String retValue = defaultValue; + + if (key != null && !key.isEmpty() && properties.get(key) != null) { + String value = properties.get(key).toString(); + // Let the value pass through because it doesn't + // start with ${ + if (!value.startsWith("${")) { + retValue = value; + } + } + + return retValue; + } + } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UnauthTokenBasedRealm.java b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UnauthTokenBasedRealm.java index 05c2bce8..e613b450 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UnauthTokenBasedRealm.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UnauthTokenBasedRealm.java @@ -1,16 +1,11 @@ package pnnl.goss.core.security.jwt; import java.text.ParseException; -import java.util.Dictionary; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.apache.felix.dm.annotation.api.Component; -import org.apache.felix.dm.annotation.api.ConfigurationDependency; -import org.apache.felix.dm.annotation.api.ServiceDependency; -import org.apache.felix.dm.annotation.api.Start; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -20,11 +15,14 @@ import org.apache.shiro.authz.permission.PermissionResolver; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.northconcepts.exception.SystemException; - import pnnl.goss.core.GossCoreContants; import pnnl.goss.core.security.GossPermissionResolver; import pnnl.goss.core.security.GossRealm; @@ -32,167 +30,154 @@ import pnnl.goss.core.security.RoleManager; import pnnl.goss.core.security.SecurityConfig; - /** - * This class handles property based authentication/authorization. It will only be - * started as a component if a pnnl.goss.core.security.properties.cfg file exists - * within the configuration directory. - * - * The format of each property should be username=password,permission1,permission2 ... where - * permission1 and permission2 are of the format domain:object:action. There can be multiple - * levels of domain object and action. An example permission string format is printers:lp2def:create - * or topic:request:subscribe. - * + * This class handles property based authentication/authorization. It will only + * be started as a component if a pnnl.goss.core.security.unauthrealm.cfg file + * exists within the configuration directory. + * + * The format of each property should be + * username=password,permission1,permission2 ... where permission1 and + * permission2 are of the format domain:object:action. There can be multiple + * levels of domain object and action. An example permission string format is + * printers:lp2def:create or topic:request:subscribe. + * * NOTE: This class assumes uniqueness of username in the properties file. - * + * * @author Craig Allwardt * */ -@Component +@Component(service = GossRealm.class, configurationPid = "pnnl.goss.core.security.unauthrealm", configurationPolicy = ConfigurationPolicy.REQUIRE) public class UnauthTokenBasedRealm extends AuthorizingRealm implements GossRealm { - - private static final String CONFIG_PID = "pnnl.goss.core.security.unauthrealm"; - private static final Logger log = LoggerFactory.getLogger(UnauthTokenBasedRealm.class); - - private final Map tokenMap = new ConcurrentHashMap<>(); -// private final Map> tokenPermissions = new ConcurrentHashMap<>(); - - @ServiceDependency - GossPermissionResolver gossPermissionResolver; - - @ServiceDependency + + private static final Logger log = LoggerFactory.getLogger(UnauthTokenBasedRealm.class); + + private final Map tokenMap = new ConcurrentHashMap<>(); + + @Reference + GossPermissionResolver gossPermissionResolver; + + @Reference private volatile SecurityConfig securityConfig; - - @ServiceDependency - private volatile RoleManager roleManager; - - @ConfigurationDependency(pid=CONFIG_PID) - public synchronized void updated(Dictionary properties) throws SystemException { - if (properties != null) { -// Enumeration keys = properties.keys(); - -// while(keys.hasMoreElements()){ -// String user = keys.nextElement(); -// String groups = properties.get(user).toString(); -// System.out.println("Registering user roles: "+user+" -- "+groups); -// List groupList = new ArrayList(Arrays.asList(StringUtils.split(groups, ','))); -// //TODO in RIGHT HERE -// roles.put(user, groupList); -// } - } - - } - - @Start - public void start(){ - } - - - - @Override - protected AuthorizationInfo doGetAuthorizationInfo( - PrincipalCollection principals) { - //get the principal this realm cares about: + + @Reference + private volatile RoleManager roleManager; + + @Activate + public void activate(Map properties) { + log.info("Activating UnauthTokenBasedRealm"); + updated(properties); + } + + @Modified + public synchronized void updated(Map properties) { + if (properties != null) { + log.debug("Updating UnauthTokenBasedRealm with {} properties", properties.size()); + } + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo( + PrincipalCollection principals) { + // get the principal this realm cares about: String username = (String) getAvailablePrincipal(principals); AuthorizationInfo accnt = tokenMap.get(username); - if(accnt==null){ - log.debug("No authorization info found for "+username); + if (accnt == null) { + log.debug("No authorization info found for " + username); } return accnt; - } + } - @Override - protected AuthenticationInfo doGetAuthenticationInfo( - AuthenticationToken token) throws AuthenticationException { - //we can safely cast to a UsernamePasswordToken here, because this class 'supports' UsernamePasswordToken - //objects. See the Realm.supports() method if your application will use a different type of token. + @Override + protected AuthenticationInfo doGetAuthenticationInfo( + AuthenticationToken token) throws AuthenticationException { + // we can safely cast to a UsernamePasswordToken here, because this class + // 'supports' UsernamePasswordToken + // objects. See the Realm.supports() method if your application will use a + // different type of token. UsernamePasswordToken upToken = (UsernamePasswordToken) token; -// upToken.setRememberMe(true); + // upToken.setRememberMe(true); SimpleAccount acnt = null; String username = upToken.getUsername(); - log.info("Get authentication info for "+username); + log.info("Get authentication info for " + username); char[] pw = upToken.getPassword(); - //If it receives a token - if (username!=null && username.length()>250 && pw.length==0) { - //Validate token - boolean verified = securityConfig.validateToken(username); - log.info("Recieved token: "+username+" verified: "+verified); - if(verified){ - //TODO get username from token, get permissions for username - - try { - // look up permissions based on roles and add them - Set permissions = new HashSet(); - JWTAuthenticationToken tokenObj = securityConfig.parseToken(username); - log.info("Has token roles: "+tokenObj.getRoles()); - - if(roleManager!=null){ - permissions = roleManager.getRolePermissions(tokenObj.getRoles()); - log.debug("Permissions for user "+username+": "+permissions); - }else { - log.warn("Role manager is null"); - } - log.info("Has role permissions: "+permissions); - acnt = new SimpleAccount(username, "", getName() ); - for(String perm: permissions){ - acnt.addStringPermission(perm); - } - tokenMap.put(username, acnt); - - } catch (ParseException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - + // If it receives a token + if (username != null && username.length() > 250 && pw.length == 0) { + // Validate token + boolean verified = securityConfig.validateToken(username); + log.info("Recieved token: " + username + " verified: " + verified); + if (verified) { + // TODO get username from token, get permissions for username + + try { + // look up permissions based on roles and add them + Set permissions = new HashSet(); + JWTAuthenticationToken tokenObj = securityConfig.parseToken(username); + log.info("Has token roles count: {}", + tokenObj.getRoles() != null ? tokenObj.getRoles().size() : 0); + + if (roleManager != null) { + permissions = roleManager.getRolePermissions(tokenObj.getRoles()); + log.debug("Permissions for user " + username + ": " + permissions); + } else { + log.warn("Role manager is null"); + } + log.info("Has role permissions: " + permissions); + acnt = new SimpleAccount(username, "", getName()); + for (String perm : permissions) { + acnt.addStringPermission(perm); + } + tokenMap.put(username, acnt); + + } catch (ParseException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + } else { - - //System user should be approved by the system realm - if("system".equals(upToken.getUsername()) ){ - return null; - } - - - String userName = upToken.getUsername(); - //todo check usenamr and pw against user repository - String loginTopic = "/topic/"+GossCoreContants.PROP_TOKEN_QUEUE; - acnt = new SimpleAccount(upToken.getUsername(), upToken.getPassword(), getName() ); - acnt.addStringPermission("topic:ActiveMQ.Advisory.Connection:create"); - acnt.addStringPermission("topic:ActiveMQ.Advisory.Queue:create"); - acnt.addStringPermission("topic:ActiveMQ.Advisory.Consumer.Queue.temp.token_resp."+userName); - acnt.addStringPermission("topic:ActiveMQ.Advisory.Consumer.Queue.temp.token_resp."+userName+"-*"); - acnt.addStringPermission("topic:"+GossCoreContants.PROP_TOKEN_QUEUE+":write"); - acnt.addStringPermission("topic:"+GossCoreContants.PROP_TOKEN_QUEUE+":create"); - acnt.addStringPermission("queue:temp.token_resp."+userName); - acnt.addStringPermission("queue:temp.token_resp."+userName+"-*"); - - - tokenMap.put(username, acnt); + + // System user should be approved by the system realm + if ("system".equals(upToken.getUsername())) { + return null; + } + + String userName = upToken.getUsername(); + // todo check usenamr and pw against user repository + String loginTopic = "/topic/" + GossCoreContants.PROP_TOKEN_QUEUE; + acnt = new SimpleAccount(upToken.getUsername(), upToken.getPassword(), getName()); + acnt.addStringPermission("topic:ActiveMQ.Advisory.Connection:create"); + acnt.addStringPermission("topic:ActiveMQ.Advisory.Queue:create"); + acnt.addStringPermission("topic:ActiveMQ.Advisory.Consumer.Queue.temp.token_resp." + userName); + acnt.addStringPermission("topic:ActiveMQ.Advisory.Consumer.Queue.temp.token_resp." + userName + "-*"); + acnt.addStringPermission("topic:" + GossCoreContants.PROP_TOKEN_QUEUE + ":write"); + acnt.addStringPermission("topic:" + GossCoreContants.PROP_TOKEN_QUEUE + ":create"); + acnt.addStringPermission("queue:temp.token_resp." + userName); + acnt.addStringPermission("queue:temp.token_resp." + userName + "-*"); + + tokenMap.put(username, acnt); } - return acnt; - } - - - @Override - public Set getPermissions(String identifier) { - //I don't believe this is used - return new HashSet<>(); - } - - @Override - public boolean hasIdentifier(String identifier) { - return tokenMap.containsKey(identifier); - } - - @Override - public PermissionResolver getPermissionResolver() { - if(gossPermissionResolver!=null) - return gossPermissionResolver; - else - return super.getPermissionResolver(); - } + return acnt; + } + + @Override + public Set getPermissions(String identifier) { + // I don't believe this is used + return new HashSet<>(); + } + + @Override + public boolean hasIdentifier(String identifier) { + return tokenMap.containsKey(identifier); + } + + @Override + public PermissionResolver getPermissionResolver() { + if (gossPermissionResolver != null) + return gossPermissionResolver; + else + return super.getPermissionResolver(); + } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserDefault.java b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserDefault.java index aed43412..ca78e64a 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserDefault.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserDefault.java @@ -11,4 +11,4 @@ default Set getRoles() { roles.add("default"); return roles; } -} \ No newline at end of file +} diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepository.java b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepository.java index 7e0dce4e..c47ff3d1 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepository.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepository.java @@ -1,21 +1,20 @@ package pnnl.goss.core.security.jwt; public interface UserRepository { - + public UserDefault findByUserId(Object userId); public UserDefault findById(Object id); -// public byte[] generateSharedKey(); - -// public long getExpirationDate() ; -// -// public String getIssuer(); + // public byte[] generateSharedKey(); + // public long getExpirationDate() ; + // + // public String getIssuer(); -// public TokenResponse createToken(UserDefault user) ; + // public TokenResponse createToken(UserDefault user) ; -// public String createToken(Object userId) ; + // public String createToken(Object userId) ; -// public boolean validateToken(String token); -} \ No newline at end of file + // public boolean validateToken(String token); +} diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepositoryImpl.java b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepositoryImpl.java index 256d25b8..ea65c468 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepositoryImpl.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/jwt/UserRepositoryImpl.java @@ -1,16 +1,23 @@ package pnnl.goss.core.security.jwt; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSObject; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.JWSVerifier; -import com.nimbusds.jose.Payload; -import com.nimbusds.jose.crypto.MACSigner; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import com.northconcepts.exception.SystemException; +import java.io.Serializable; +import java.util.Base64; + +import jakarta.jms.Destination; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.shiro.authc.SimpleAccount; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pnnl.goss.core.Client; import pnnl.goss.core.Client.PROTOCOL; @@ -21,178 +28,138 @@ import pnnl.goss.core.security.RoleManager; import pnnl.goss.core.security.SecurityConfig; -import java.io.Serializable; +@Component(service = UserRepository.class, configurationPid = "pnnl.goss.core.security.userfile", configurationPolicy = ConfigurationPolicy.REQUIRE) +public class UserRepositoryImpl implements UserRepository { -//import java.io.Serializable; -import java.security.SecureRandom; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Date; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; + @Reference + private volatile SecurityConfig securityConfig; -import org.apache.felix.dm.annotation.api.Component; -import org.apache.felix.dm.annotation.api.ConfigurationDependency; -import org.apache.felix.dm.annotation.api.ServiceDependency; -import org.apache.felix.dm.annotation.api.Start; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.shiro.authc.SimpleAccount; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + @Reference + private volatile ClientFactory clientFactory; + + @Reference + private volatile RoleManager roleManager; -@Component -public class UserRepositoryImpl implements UserRepository{ - - @ServiceDependency - private volatile SecurityConfig securityConfig; - - @ServiceDependency - private volatile ClientFactory clientFactory; - - @ServiceDependency - private volatile RoleManager roleManager; - - - //These should probably come from config -// private static final String ISSUED_BY = "GridOPTICS Software System"; -// private byte[] sharedKey = securityConfig.getTokenSecret(); -// private byte[] sharedKey = generateSharedKey(); - - private static final String CONFIG_PID = "pnnl.goss.core.security.userfile"; - private static final Logger log = LoggerFactory.getLogger(UserRepositoryImpl.class); - private final String realmName = UnauthTokenBasedRealm.class.getName();//JWTRealm.class.getName(); - private final Map userMap = new ConcurrentHashMap<>(); -// private final Map> userPermissions = new ConcurrentHashMap<>(); - private final Map> userRoles = new ConcurrentHashMap<>(); - private final Map tokenMap = new ConcurrentHashMap<>(); - - - public UserDefault findByUserId(Object userId){return null;} - - public UserDefault findById(Object id){return null;} - - - - -// private byte[] getSharedKey(){return sharedKey;} - -// public TokenResponse createToken(UserDefault user) { -// TokenResponse response = new TokenResponse(user, createToken(user.getPrincipal())); -// return response; -// } - - - - - - - - @Start - public void start(){ - try { - Client client = clientFactory.create(PROTOCOL.STOMP, - new UsernamePasswordCredentials(securityConfig.getManagerUser(), securityConfig.getManagerPassword()), false); - //test publish to make sure the topic exists - client.publish("ActiveMQ.Advisory.Connection", ""); - String loginTopic = "/topic/"+GossCoreContants.PROP_TOKEN_QUEUE; - client.subscribe(loginTopic, new ResponseEvent(client)); - }catch (Exception e) { - e.printStackTrace(); - } + private static final Logger log = LoggerFactory.getLogger(UserRepositoryImpl.class); + private final String realmName = UnauthTokenBasedRealm.class.getName(); + private final Map userMap = new ConcurrentHashMap<>(); + private final Map> userRoles = new ConcurrentHashMap<>(); + private final Map tokenMap = new ConcurrentHashMap<>(); + + public UserDefault findByUserId(Object userId) { + return null; + } + + public UserDefault findById(Object id) { + return null; } - - @ConfigurationDependency(pid=CONFIG_PID) - public synchronized void updated(Dictionary properties) throws SystemException { - - - if (properties != null){ - log.debug("Updating User Repository Impl"); - userMap.clear(); - userRoles.clear(); - - Enumeration keys = properties.keys(); - - while(keys.hasMoreElements()){ - String k = keys.nextElement(); - String v = (String)properties.get(k); - String[] credAndPermissions = v.split(","); - Set perms = new HashSet<>(); - SimpleAccount acnt = new SimpleAccount(k, credAndPermissions[0], realmName); - for(int i =1; i properties) { + log.info("Activating UserRepositoryImpl"); + updated(properties); + start(); + } + + private void start() { + try { + Client client = clientFactory.create(PROTOCOL.STOMP, + new UsernamePasswordCredentials(securityConfig.getManagerUser(), + securityConfig.getManagerPassword()), + false); + // test publish to make sure the topic exists + client.publish("ActiveMQ.Advisory.Connection", ""); + String loginTopic = "/topic/" + GossCoreContants.PROP_TOKEN_QUEUE; + log.info("UserRepositoryImpl subscribing to token topic: {}", loginTopic); + client.subscribe(loginTopic, new ResponseEvent(client)); + } catch (Exception e) { + log.error("Error starting UserRepositoryImpl", e); + } + } + + @Modified + public synchronized void updated(Map properties) { + + if (properties != null) { + log.debug("Updating User Repository Impl"); + userMap.clear(); + userRoles.clear(); + + for (String k : properties.keySet()) { + // Skip OSGi/ConfigAdmin metadata properties + if (k.startsWith("service.") || k.startsWith("component.") || + k.startsWith("felix.") || k.equals("osgi.ds.satisfying.condition.target")) { + continue; + } + + Object value = properties.get(k); + if (!(value instanceof String)) { + continue; + } + + String v = (String) value; + String[] credAndPermissions = v.split(","); + Set perms = new HashSet<>(); + SimpleAccount acnt = new SimpleAccount(k, credAndPermissions[0], realmName); + for (int i = 1; i < credAndPermissions.length; i++) { + acnt.addStringPermission(credAndPermissions[i]); + perms.add(credAndPermissions[i]); + } + userMap.put(k, acnt); + userRoles.put(k, perms); + } + log.info("UserRepositoryImpl configured with {} users", userMap.size()); + } + + } + + class ResponseEvent implements GossResponseEvent { + private final Client client; + + public ResponseEvent(Client client) { + this.client = client; + } + + @Override + public void onMessage(Serializable response) { + log.debug("Received token request"); + String responseData = "{}"; + if (response instanceof DataResponse) { + String base64Auth = (String) ((DataResponse) response).getData(); + String userAauthStr = new String(Base64.getDecoder().decode(base64Auth.trim().getBytes())); + String[] authArr = userAauthStr.split(":"); + String userId = authArr[0]; + // validate submitted username and password before generating token + if (userMap.containsKey(userId) && authArr[1].equals(userMap.get(userId).getCredentials())) { + // Create token + String token = null; + if (tokenMap.containsKey(userId)) { + token = tokenMap.get(userId); + log.debug("Token already exists for " + userId); + } else { + token = securityConfig.createToken(authArr[0], userRoles.get(userId)); + log.debug("Created token for " + userId); + tokenMap.put(userId, token); + } + responseData = token; + + } else { + log.debug("Authentication failed for " + userId); + + // Send authentication failed message + responseData = "authentication failed"; + } + Destination replyDest = ((DataResponse) response).getReplyDestination(); + if (replyDest != null) { + log.info("Returning token for user " + userId + " on destination " + replyDest); + client.publish(replyDest, responseData); + } else { + log.debug("No reply destination for token request from user " + userId + " - ignoring"); + } + } else { + client.publish("goss/management/response", responseData); + } + } + } - - - - - - -class ResponseEvent implements GossResponseEvent{ - private final Client client; -// private Gson gson = new Gson(); - - public ResponseEvent(Client client){ - this.client = client; - } - - @Override - public void onMessage(Serializable response) { - log.debug("Received token request"); - String responseData = "{}"; - if (response instanceof DataResponse){ - String base64Auth = (String)((DataResponse) response).getData(); - String userAauthStr = new String(Base64.getDecoder().decode(base64Auth.trim().getBytes())); - String[] authArr = userAauthStr.split(":"); - String userId = authArr[0]; - //validate submitted username and password before generating token - if(userMap.containsKey(userId) && authArr[1].equals(userMap.get(userId).getCredentials())){ - //Create token - String token = null; - if(tokenMap.containsKey(userId)){ - token=tokenMap.get(userId); - log.debug("Token already exists for "+userId); - } else { - token = securityConfig.createToken(authArr[0], userRoles.get(userId.toString())); - log.debug("Created token for "+userId); - tokenMap.put(userId, token); - } - responseData = token; - - } else { - log.debug("Authentication failed for "+userId); - - //Send authentication failed message - responseData = "authentication failed"; - } - log.info("Returning token for user "+userId+" on destination "+((DataResponse) response).getReplyDestination()); - - client.publish(((DataResponse) response).getReplyDestination(), responseData); - } else { - client.publish("goss/management/response", responseData); - } - } - - } -// private byte[] generateSharedKey() { -// SecureRandom random = new SecureRandom(); -// byte[] sharedKey = new byte[32]; -// random.nextBytes(sharedKey); -// return sharedKey; -// } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/propertyfile/PropertyBasedRealm.java b/pnnl.goss.core/src/pnnl/goss/core/security/propertyfile/PropertyBasedRealm.java index c353bae8..ca634167 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/propertyfile/PropertyBasedRealm.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/propertyfile/PropertyBasedRealm.java @@ -78,7 +78,13 @@ protected AuthenticationInfo doGetAuthenticationInfo( // objects. See the Realm.supports() method if your application will use a // different type of token. UsernamePasswordToken upToken = (UsernamePasswordToken) token; - return userMap.get(upToken.getUsername()); + String username = upToken.getUsername(); + if (username == null) { + log.warn("Authentication attempt with null username (client may be using " + + "token-based auth against a server without token support)"); + return null; + } + return userMap.get(username); } @Modified diff --git a/pnnl.goss.core/src/pnnl/goss/core/security/system/SystemBasedRealm.java b/pnnl.goss.core/src/pnnl/goss/core/security/system/SystemBasedRealm.java index b89f86e3..4b7cf79e 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/security/system/SystemBasedRealm.java +++ b/pnnl.goss.core/src/pnnl/goss/core/security/system/SystemBasedRealm.java @@ -1,15 +1,10 @@ package pnnl.goss.core.security.system; -import java.util.Dictionary; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.apache.felix.dm.annotation.api.Component; -import org.apache.felix.dm.annotation.api.ConfigurationDependency; -import org.apache.felix.dm.annotation.api.ServiceDependency; -import org.apache.felix.dm.annotation.api.Start; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -19,105 +14,109 @@ import org.apache.shiro.authz.permission.PermissionResolver; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.northconcepts.exception.SystemException; - import pnnl.goss.core.security.GossPermissionResolver; import pnnl.goss.core.security.GossRealm; import pnnl.goss.core.security.SecurityConfig; - /** - * This class handles property based authentication/authorization. It will only be - * started as a component if a pnnl.goss.core.security.properties.cfg file exists - * within the configuration directory. - * - * The format of each property should be username=password,permission1,permission2 ... where - * permission1 and permission2 are of the format domain:object:action. There can be multiple - * levels of domain object and action. An example permission string format is printers:lp2def:create - * or topic:request:subscribe. - * + * This class handles property based authentication/authorization. It will only + * be started as a component if a pnnl.goss.core.security.systemrealm.cfg file + * exists within the configuration directory. + * + * The format of each property should be + * username=password,permission1,permission2 ... where permission1 and + * permission2 are of the format domain:object:action. There can be multiple + * levels of domain object and action. An example permission string format is + * printers:lp2def:create or topic:request:subscribe. + * * NOTE: This class assumes uniqueness of username in the properties file. - * + * * @author Craig Allwardt * */ -@Component +@Component(service = GossRealm.class, configurationPid = "pnnl.goss.core.security.systemrealm", configurationPolicy = ConfigurationPolicy.REQUIRE) public class SystemBasedRealm extends AuthorizingRealm implements GossRealm { - - private static final String CONFIG_PID = "pnnl.goss.core.security.systemrealm"; - private static final Logger log = LoggerFactory.getLogger(SystemBasedRealm.class); - - private final Map userMap = new ConcurrentHashMap<>(); - private final Map> userPermissions = new ConcurrentHashMap<>(); - - @ServiceDependency - GossPermissionResolver gossPermissionResolver; - - @ServiceDependency + + private static final Logger log = LoggerFactory.getLogger(SystemBasedRealm.class); + + private final Map userMap = new ConcurrentHashMap<>(); + private final Map> userPermissions = new ConcurrentHashMap<>(); + + @Reference + GossPermissionResolver gossPermissionResolver; + + @Reference private volatile SecurityConfig securityConfig; - - @Override - protected void onInit() { - super.onInit(); - Set perms = new HashSet<>(); - - SimpleAccount acnt = new SimpleAccount(securityConfig.getManagerUser(), securityConfig.getManagerPassword(), getName() ); - acnt.addStringPermission("queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:write"); - perms.add("queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:write"); - userMap.put(securityConfig.getManagerUser(), acnt); - userPermissions.put(securityConfig.getManagerUser(), perms); - } - - - @Start - public void start(){ - } - - @ConfigurationDependency(pid=CONFIG_PID) - public synchronized void updated(Dictionary properties) throws SystemException { - } - - @Override - protected AuthorizationInfo doGetAuthorizationInfo( - PrincipalCollection principals) { - - //get the principal this realm cares about: + + @Override + protected void onInit() { + super.onInit(); + Set perms = new HashSet<>(); + + SimpleAccount acnt = new SimpleAccount(securityConfig.getManagerUser(), securityConfig.getManagerPassword(), + getName()); + acnt.addStringPermission("queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:write"); + perms.add("queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:write"); + userMap.put(securityConfig.getManagerUser(), acnt); + userPermissions.put(securityConfig.getManagerUser(), perms); + } + + @Activate + public void activate(Map properties) { + log.info("Activating SystemBasedRealm"); + } + + @Modified + public synchronized void updated(Map properties) { + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo( + PrincipalCollection principals) { + + // get the principal this realm cares about: String username = (String) getAvailablePrincipal(principals); return userMap.get(username); - } + } - @Override - protected AuthenticationInfo doGetAuthenticationInfo( - AuthenticationToken token) throws AuthenticationException { - //we can safely cast to a UsernamePasswordToken here, because this class 'supports' UsernamePasswordToken - //objects. See the Realm.supports() method if your application will use a different type of token. + @Override + protected AuthenticationInfo doGetAuthenticationInfo( + AuthenticationToken token) throws AuthenticationException { + // we can safely cast to a UsernamePasswordToken here, because this class + // 'supports' UsernamePasswordToken + // objects. See the Realm.supports() method if your application will use a + // different type of token. UsernamePasswordToken upToken = (UsernamePasswordToken) token; upToken.setRememberMe(true); return userMap.get(upToken.getUsername()); - } - - - @Override - public Set getPermissions(String identifier) { - if (hasIdentifier(identifier)){ - return userPermissions.get(identifier); - } - return new HashSet<>(); - } - - @Override - public boolean hasIdentifier(String identifier) { - return userMap.containsKey(identifier); - } - - @Override - public PermissionResolver getPermissionResolver() { - if(gossPermissionResolver!=null) - return gossPermissionResolver; - else - return super.getPermissionResolver(); - } + } + + @Override + public Set getPermissions(String identifier) { + if (hasIdentifier(identifier)) { + return userPermissions.get(identifier); + } + return new HashSet<>(); + } + + @Override + public boolean hasIdentifier(String identifier) { + return userMap.containsKey(identifier); + } + + @Override + public PermissionResolver getPermissionResolver() { + if (gossPermissionResolver != null) + return gossPermissionResolver; + else + return super.getPermissionResolver(); + } } diff --git a/pnnl.goss.core/src/pnnl/goss/core/server/impl/ServerListener.java b/pnnl.goss.core/src/pnnl/goss/core/server/impl/ServerListener.java index 5dced4f1..c190d2a5 100644 --- a/pnnl.goss.core/src/pnnl/goss/core/server/impl/ServerListener.java +++ b/pnnl.goss.core/src/pnnl/goss/core/server/impl/ServerListener.java @@ -46,6 +46,7 @@ import java.io.Serializable; +import jakarta.jms.Destination; import jakarta.jms.InvalidDestinationException; import jakarta.jms.JMSException; import jakarta.jms.Message; @@ -53,6 +54,8 @@ import jakarta.jms.ObjectMessage; import jakarta.jms.Session; +import org.apache.activemq.command.ActiveMQQueue; +import org.apache.activemq.command.ActiveMQTopic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,6 +95,32 @@ public ServerListener setRegistryHandler(RequestHandlerRegistry registry) { return this; } + /** + * Get the reply destination from a JMS message, falling back to string + * properties when the STOMP adapter can't parse a bare reply-to header into a + * JMS Destination (e.g., "feeder-models" instead of "/queue/feeder-models"). + */ + private Destination getReplyDestination(Message msg) { + try { + Destination dest = msg.getJMSReplyTo(); + if (dest != null) { + if (dest instanceof ActiveMQQueue || dest instanceof ActiveMQTopic) + return dest; + log.debug("Normalizing reply destination: {} -> Queue", dest); + return session.createQueue(dest.toString()); + } + // JMSReplyTo is null - check for bare reply-to string property + String replyTo = msg.getStringProperty("reply-to"); + if (replyTo != null && !replyTo.isEmpty()) { + log.debug("Using bare reply-to property as queue: {}", replyTo); + return session.createQueue(replyTo); + } + } catch (JMSException e) { + log.warn("Failed to get reply destination", e); + } + return null; + } + public void onMessage(Message message1) { final Message message = message1; @@ -100,7 +129,7 @@ public void onMessage(Message message1) { Thread thread = new Thread(new Runnable() { public void run() { ServerPublisher serverPublisher = new ServerPublisher(session); - String username = ""; + String username = ""; try { ObjectMessage objectMessage = (ObjectMessage) message; @@ -113,7 +142,8 @@ public void run() { if (useAuth) { if (!message.getBooleanProperty(SecurityConstants.HAS_SUBJECT_HEADER)) { log.error("Identifier not set in message header"); - serverPublisher.sendErrror("Invalid subject in message!", message.getJMSReplyTo()); + serverPublisher.sendErrror("Invalid subject in message!", + getReplyDestination(message)); return; } @@ -125,7 +155,8 @@ public void run() { if (!allowed) { log.info("Access denied to " + identifier + " for request type " + request.getClass().getName()); - serverPublisher.sendErrror("Access Denied for the requested data", message.getJMSReplyTo()); + serverPublisher.sendErrror("Access Denied for the requested data", + getReplyDestination(message)); return; } log.debug("Access allowed to the request"); @@ -141,13 +172,13 @@ public void run() { UploadResponse response = (UploadResponse) handlerRegistry.handle(dataType, data); response.setId(request.getId()); - serverPublisher.sendResponse(response, message.getJMSReplyTo()); + serverPublisher.sendResponse(response, getReplyDestination(message)); // TODO: Added capability for event processing without upload. Example - FNCS /* * UploadResponse response = new UploadResponse(true); * response.setId(request.getId()); serverPublisher.sendResponse(response, - * message.getJMSReplyTo()); + * getReplyDestination(message)); */ if (data instanceof Event) { @@ -164,7 +195,8 @@ public void run() { log.error("Upload request failed!" + e); UploadResponse uploadResponse = new UploadResponse(false); uploadResponse.setMessage(e.getMessage()); - serverPublisher.sendResponse(uploadResponse, message.getJMSReplyTo()); + serverPublisher.sendResponse(uploadResponse, + getReplyDestination(message)); serverPublisher.close(); } } else if (request instanceof RequestAsync) { @@ -178,10 +210,11 @@ public void run() { response.setId(request.getId()); if (message.getStringProperty("RESPONSE_FORMAT") != null) { - serverPublisher.sendResponse(response, message.getJMSReplyTo(), + serverPublisher.sendResponse(response, getReplyDestination(message), RESPONSE_FORMAT.valueOf(message.getStringProperty("RESPONSE_FORMAT"))); } else { - serverPublisher.sendResponse(response, message.getJMSReplyTo(), null); + serverPublisher.sendResponse(response, getReplyDestination(message), + null); } while (response.isResponseComplete() == false) { @@ -190,10 +223,12 @@ public void run() { response.setId(request.getId()); if (message.getStringProperty("RESPONSE_FORMAT") != null) { - serverPublisher.sendResponse(response, message.getJMSReplyTo(), + serverPublisher.sendResponse(response, + getReplyDestination(message), RESPONSE_FORMAT.valueOf(message.getStringProperty("RESPONSE_FORMAT"))); } else { - serverPublisher.sendResponse(response, message.getJMSReplyTo(), null); + serverPublisher.sendResponse(response, + getReplyDestination(message), null); } } } else { @@ -206,10 +241,11 @@ public void run() { response.setId(request.getId()); if (message.getStringProperty("RESPONSE_FORMAT") != null) - serverPublisher.sendResponse(response, message.getJMSReplyTo(), + serverPublisher.sendResponse(response, getReplyDestination(message), RESPONSE_FORMAT.valueOf(message.getStringProperty("RESPONSE_FORMAT"))); else - serverPublisher.sendResponse(response, message.getJMSReplyTo(), null); + serverPublisher.sendResponse(response, getReplyDestination(message), + null); // System.out.println(System.currentTimeMillis()); } @@ -219,7 +255,7 @@ public void run() { try { serverPublisher.sendResponse( new DataResponse(new DataError("Exception occured: " + e.getMessage())), - message.getJMSReplyTo()); + getReplyDestination(message)); } catch (JMSException e1) { // TODO Auto-generated catch block e1.printStackTrace(); @@ -230,7 +266,7 @@ public void run() { e.printStackTrace(); try { serverPublisher.sendResponse(new DataResponse(new DataError("Exception occured")), - message.getJMSReplyTo()); + getReplyDestination(message)); } catch (JMSException e1) { // TODO Auto-generated catch block e1.printStackTrace(); diff --git a/pnnl.goss.core/src/pnnl/goss/server/registry/PooledBasicDataSourceBuilderImpl.java b/pnnl.goss.core/src/pnnl/goss/server/registry/PooledBasicDataSourceBuilderImpl.java index e5d82695..6fa2a155 100644 --- a/pnnl.goss.core/src/pnnl/goss/server/registry/PooledBasicDataSourceBuilderImpl.java +++ b/pnnl.goss.core/src/pnnl/goss/server/registry/PooledBasicDataSourceBuilderImpl.java @@ -76,10 +76,25 @@ public void create(String dsName, Properties properties) throws Exception { log.debug("Creating BasicDataSource\n\tURI:" + properties.getProperty("url") + "\n\tUser:\n\t" + properties.getProperty("username")); - Class.forName(properties.getProperty("driverClassName")); - - DataSource ds = BasicDataSourceFactory.createDataSource(properties); - - registry.add(dsName, new DataSourceObjectImpl(dsName, DataSourceType.DS_TYPE_JDBC, ds)); + // Use this bundle's classloader so that OSGi DynamicImport-Package can + // resolve the JDBC driver from the MySQL bundle. The thread context + // classloader (often the app classloader) cannot see OSGi bundles. + ClassLoader bundleCl = PooledBasicDataSourceBuilderImpl.class.getClassLoader(); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(bundleCl); + Class.forName(properties.getProperty("driverClassName"), true, bundleCl); + DataSource ds = BasicDataSourceFactory.createDataSource(properties); + // Force DBCP's lazy driver loading while TCCL is still set. + // DBCP 1.4 caches the connection factory after first getConnection(). + try { + ds.getConnection().close(); + } catch (Exception e) { + log.debug("Initial connection probe failed (database may not be ready): {}", e.getMessage()); + } + registry.add(dsName, new DataSourceObjectImpl(dsName, DataSourceType.DS_TYPE_JDBC, ds)); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } } } diff --git a/scripts/version.py b/scripts/version.py index 5076006f..2a3c5332 100755 --- a/scripts/version.py +++ b/scripts/version.py @@ -44,6 +44,9 @@ def find_bnd_files(root: Path) -> list[Path]: """Find all .bnd files that contain Bundle-Version.""" bnd_files = [] for bnd_file in root.rglob('*.bnd'): + # Skip directories that happen to match *.bnd + if not bnd_file.is_file(): + continue # Skip cnf/ext directory (these are config files, not bundles) if 'cnf/ext' in str(bnd_file): continue @@ -157,14 +160,17 @@ def get_current_version(root: Path) -> str | None: base_version = version.replace('-SNAPSHOT', '') versions.add(base_version) - if len(versions) == 0: + # Filter to only semver-like versions (all parts are numeric) + semver_versions = {v for v in versions if all(p.isdigit() for p in v.split('.'))} + + if len(semver_versions) == 0: return None - if len(versions) > 1: - log_warn(f"Multiple versions found: {sorted(versions)}") + if len(semver_versions) > 1: + log_warn(f"Multiple versions found: {sorted(semver_versions)}") # Return the highest version - return sorted(versions, key=lambda v: [int(x) for x in v.split('.')])[-1] + return sorted(semver_versions, key=lambda v: [int(x) for x in v.split('.')])[-1] - return versions.pop() + return semver_versions.pop() def bump_version(version: str, bump_type: str) -> str: