Repository: google/tsunami-security-scanner
Branch: master
Commit: 32920de1868c
Files: 345
Total size: 1.7 MB
Directory structure:
gitextract_306kc4re/
├── .dockerignore
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── core-build.yml
│ ├── core-push.yml
│ ├── devel-push.yml
│ └── full-push.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── common/
│ ├── README.md
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── tsunami/
│ │ └── common/
│ │ ├── ErrorCode.java
│ │ ├── TsunamiException.java
│ │ ├── cli/
│ │ │ ├── CliOption.java
│ │ │ └── CliOptionsModule.java
│ │ ├── command/
│ │ │ ├── CommandExecutionThreadPool.java
│ │ │ ├── CommandExecutor.java
│ │ │ ├── CommandExecutorFactory.java
│ │ │ └── CommandExecutorModule.java
│ │ ├── concurrent/
│ │ │ ├── BaseThreadPoolModule.java
│ │ │ ├── ScheduledThreadPoolModule.java
│ │ │ └── ThreadPoolModule.java
│ │ ├── config/
│ │ │ ├── ConfigException.java
│ │ │ ├── ConfigLoader.java
│ │ │ ├── ConfigModule.java
│ │ │ ├── TsunamiConfig.java
│ │ │ ├── YamlConfigLoader.java
│ │ │ └── annotations/
│ │ │ └── ConfigProperties.java
│ │ ├── data/
│ │ │ ├── NetworkEndpointUtils.java
│ │ │ └── NetworkServiceUtils.java
│ │ ├── io/
│ │ │ └── archiving/
│ │ │ ├── Archiver.java
│ │ │ ├── GoogleCloudStorageArchiver.java
│ │ │ ├── GoogleCloudStorageArchiverModule.java
│ │ │ ├── RawFileArchiver.java
│ │ │ └── testing/
│ │ │ ├── FakeArchiver.java
│ │ │ ├── FakeGoogleCloudStorageArchivers.java
│ │ │ ├── FakeGoogleCloudStorageArchiversModule.java
│ │ │ ├── FakeRawFileArchiver.java
│ │ │ └── FakeRawFileArchiverModule.java
│ │ ├── net/
│ │ │ ├── FuzzingUtils.java
│ │ │ ├── UrlUtils.java
│ │ │ ├── db/
│ │ │ │ ├── ConnectionProvider.java
│ │ │ │ └── ConnectionProviderInterface.java
│ │ │ ├── http/
│ │ │ │ ├── HttpClient.java
│ │ │ │ ├── HttpClientCliOptions.java
│ │ │ │ ├── HttpClientConfigProperties.java
│ │ │ │ ├── HttpClientModule.java
│ │ │ │ ├── HttpHeaders.java
│ │ │ │ ├── HttpMethod.java
│ │ │ │ ├── HttpRequest.java
│ │ │ │ ├── HttpResponse.java
│ │ │ │ ├── HttpStatus.java
│ │ │ │ ├── OkHttpHttpClient.java
│ │ │ │ └── javanet/
│ │ │ │ ├── ConnectionFactory.java
│ │ │ │ └── DefaultConnectionFactory.java
│ │ │ └── socket/
│ │ │ ├── DefaultTsunamiSocketFactory.java
│ │ │ ├── TsunamiSocketFactory.java
│ │ │ ├── TsunamiSocketFactoryCliOptions.java
│ │ │ ├── TsunamiSocketFactoryConfigProperties.java
│ │ │ └── TsunamiSocketFactoryModule.java
│ │ ├── reflection/
│ │ │ ├── ClassGraphModule.java
│ │ │ └── RuntimeClassGraphScanResult.java
│ │ ├── server/
│ │ │ ├── CompactRunRequestHelper.java
│ │ │ └── LanguageServerCommand.java
│ │ ├── time/
│ │ │ ├── SystemUtcClockModule.java
│ │ │ ├── UtcClock.java
│ │ │ └── testing/
│ │ │ ├── FakeUtcClock.java
│ │ │ └── FakeUtcClockModule.java
│ │ └── version/
│ │ ├── ComparisonUtility.java
│ │ ├── KnownQualifier.java
│ │ ├── Segment.java
│ │ ├── Token.java
│ │ ├── Version.java
│ │ ├── VersionRange.java
│ │ └── VersionSet.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── tsunami/
│ │ └── common/
│ │ ├── cli/
│ │ │ └── CliOptionsModuleTest.java
│ │ ├── command/
│ │ │ ├── CommandExecutorFactoryTest.java
│ │ │ └── CommandExecutorTest.java
│ │ ├── concurrent/
│ │ │ ├── BaseThreadPoolModuleTest.java
│ │ │ ├── ScheduledThreadPoolModuleTest.java
│ │ │ └── ThreadPoolModuleTest.java
│ │ ├── config/
│ │ │ ├── ConfigModuleTest.java
│ │ │ ├── TsunamiConfigTest.java
│ │ │ └── YamlConfigLoaderTest.java
│ │ ├── data/
│ │ │ ├── NetworkEndpointUtilsTest.java
│ │ │ └── NetworkServiceUtilsTest.java
│ │ ├── io/
│ │ │ └── archiving/
│ │ │ ├── ArchiverTestUtils.java
│ │ │ ├── GoogleCloudStorageArchiverTest.java
│ │ │ └── RawFileArchiverTest.java
│ │ ├── net/
│ │ │ ├── FuzzingUtilsTest.java
│ │ │ ├── UrlUtilsTest.java
│ │ │ ├── http/
│ │ │ │ ├── HttpClientModuleTest.java
│ │ │ │ ├── HttpHeadersTest.java
│ │ │ │ ├── HttpRequestTest.java
│ │ │ │ ├── HttpResponseTest.java
│ │ │ │ └── OkHttpHttpClientTest.java
│ │ │ └── socket/
│ │ │ ├── DefaultTsunamiSocketFactoryTest.java
│ │ │ ├── TsunamiSocketFactoryCliOptionsTest.java
│ │ │ └── TsunamiSocketFactoryModuleTest.java
│ │ ├── server/
│ │ │ └── CompactRunRequestHelperTest.java
│ │ ├── time/
│ │ │ ├── SystemUtcClockModuleTest.java
│ │ │ └── testing/
│ │ │ ├── FakeUtcClockModuleTest.java
│ │ │ └── FakeUtcClockTest.java
│ │ └── version/
│ │ ├── ComparisonUtilityTest.java
│ │ ├── EqualsTestCase.java
│ │ ├── KnownQualifierTest.java
│ │ ├── LessThanTestCase.java
│ │ ├── SegmentTest.java
│ │ ├── TokenTest.java
│ │ ├── VersionRangeTest.java
│ │ ├── VersionSetTest.java
│ │ └── VersionTest.java
│ └── resources/
│ └── com/
│ └── google/
│ └── tsunami/
│ └── common/
│ └── net/
│ └── http/
│ └── testdata/
│ ├── README.md
│ └── tsunami_test_server.p12
├── core.Dockerfile
├── devel.Dockerfile
├── docs/
│ ├── _config.yml
│ ├── _data/
│ │ └── nav.yml
│ ├── _includes/
│ │ └── nav.html
│ ├── _layouts/
│ │ ├── default.html
│ │ ├── home.html
│ │ └── post.html
│ ├── _posts/
│ │ ├── 2024-03-19-tsunami-network-scanner-ai-security.md
│ │ ├── 2025-06-18-changes-to-tsunami.md
│ │ └── 2025-10-16-october-update-tsunami-prp.md
│ ├── about/
│ │ └── index.md
│ ├── assets/
│ │ └── css/
│ │ └── style.scss
│ ├── blog/
│ │ └── index.html
│ ├── contribute/
│ │ ├── code-of-conduct.md
│ │ ├── contributing.md
│ │ └── index.md
│ ├── howto/
│ │ ├── common-patterns.md
│ │ ├── howto.md
│ │ ├── index.md
│ │ ├── new-detector/
│ │ │ ├── new-detector-java.md
│ │ │ └── templated/
│ │ │ ├── 00-getting-started.md
│ │ │ ├── 01-introduction.md
│ │ │ ├── 02-bootstrapping.md
│ │ │ ├── 03-first-actions.md
│ │ │ ├── 04-workflows.md
│ │ │ ├── 05-variables.md
│ │ │ ├── 06-callback-server.md
│ │ │ ├── 07-cleanup-actions.md
│ │ │ ├── 08-writing-unit-tests.md
│ │ │ ├── appendix-naming-actions.md
│ │ │ ├── appendix-naming-plugin.md
│ │ │ ├── appendix-naming-tests.md
│ │ │ ├── appendix-using-linter.md
│ │ │ ├── glossary-predefined-variables.md
│ │ │ └── glossary-tests-magic-uri.md
│ │ └── orchestration.md
│ └── index.md
├── full.Dockerfile
├── go.mod
├── main/
│ ├── README.md
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── tsunami/
│ │ └── main/
│ │ └── cli/
│ │ ├── LanguageServerOptions.java
│ │ ├── ScanResultsArchiver.java
│ │ ├── ScanResultsArchiverModule.java
│ │ ├── TsunamiCli.java
│ │ ├── option/
│ │ │ ├── MainCliOptions.java
│ │ │ ├── OutputDataFormat.java
│ │ │ └── validator/
│ │ │ ├── IpV4Validator.java
│ │ │ ├── IpV6Validator.java
│ │ │ └── IpValidator.java
│ │ └── server/
│ │ ├── RemoteServerLoader.java
│ │ └── RemoteServerLoaderModule.java
│ └── test/
│ └── java/
│ └── com/
│ └── google/
│ └── tsunami/
│ └── main/
│ └── cli/
│ ├── LanguageServerOptionsTest.java
│ ├── ScanResultsArchiverTest.java
│ ├── TsunamiCliTest.java
│ ├── option/
│ │ ├── MainCliOptionsTest.java
│ │ ├── OutputDataFormatTest.java
│ │ └── validator/
│ │ ├── IpV4ValidatorTest.java
│ │ ├── IpV6ValidatorTest.java
│ │ └── IpValidatorTest.java
│ └── server/
│ └── RemoteServerLoaderTest.java
├── plugin/
│ ├── README.md
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── google/
│ │ │ └── tsunami/
│ │ │ └── plugin/
│ │ │ ├── LanguageServerException.java
│ │ │ ├── PluginBootstrapModule.java
│ │ │ ├── PluginDefinition.java
│ │ │ ├── PluginExecutionException.java
│ │ │ ├── PluginExecutionModule.java
│ │ │ ├── PluginExecutionResult.java
│ │ │ ├── PluginExecutionThreadPool.java
│ │ │ ├── PluginExecutor.java
│ │ │ ├── PluginExecutorImpl.java
│ │ │ ├── PluginExecutorModule.java
│ │ │ ├── PluginLoadingModule.java
│ │ │ ├── PluginManager.java
│ │ │ ├── PluginManagerCliOptions.java
│ │ │ ├── PluginServiceClient.java
│ │ │ ├── PluginType.java
│ │ │ ├── PortScanner.java
│ │ │ ├── RemoteVulnDetector.java
│ │ │ ├── RemoteVulnDetectorImpl.java
│ │ │ ├── RemoteVulnDetectorLoadingModule.java
│ │ │ ├── ServiceFingerprinter.java
│ │ │ ├── TcsClient.java
│ │ │ ├── TcsClientCliOptions.java
│ │ │ ├── TcsConfigProperties.java
│ │ │ ├── TsunamiPlugin.java
│ │ │ ├── VulnDetector.java
│ │ │ ├── annotations/
│ │ │ │ ├── ForOperatingSystemClass.java
│ │ │ │ ├── ForServiceName.java
│ │ │ │ ├── ForSoftware.java
│ │ │ │ ├── ForWebService.java
│ │ │ │ ├── PluginInfo.java
│ │ │ │ └── RequiresCallbackServer.java
│ │ │ ├── payload/
│ │ │ │ ├── NotImplementedException.java
│ │ │ │ ├── Payload.java
│ │ │ │ ├── PayloadGenerator.java
│ │ │ │ ├── PayloadGeneratorModule.java
│ │ │ │ ├── PayloadSecretGenerator.java
│ │ │ │ ├── README.md
│ │ │ │ ├── Validator.java
│ │ │ │ └── testing/
│ │ │ │ ├── FakePayloadGeneratorModule.java
│ │ │ │ └── PayloadTestHelper.java
│ │ │ └── testing/
│ │ │ ├── FailedPortScanner.java
│ │ │ ├── FailedPortScannerBootstrapModule.java
│ │ │ ├── FailedRemoteVulnDetector.java
│ │ │ ├── FailedRemoteVulnDetectorBootstrapModule.java
│ │ │ ├── FailedServiceFingerprinter.java
│ │ │ ├── FailedServiceFingerprinterBootstrapModule.java
│ │ │ ├── FailedVulnDetector.java
│ │ │ ├── FailedVulnDetectorBootstrapModule.java
│ │ │ ├── FakePluginExecutionModule.java
│ │ │ ├── FakePortScanner.java
│ │ │ ├── FakePortScanner2.java
│ │ │ ├── FakePortScannerBootstrapModule.java
│ │ │ ├── FakePortScannerBootstrapModule2.java
│ │ │ ├── FakePortScannerBootstrapModuleEmpty.java
│ │ │ ├── FakePortScannerEmpty.java
│ │ │ ├── FakeRemoteVulnDetector.java
│ │ │ ├── FakeRemoteVulnDetectorBootstrapModule.java
│ │ │ ├── FakeServiceFingerprinter.java
│ │ │ ├── FakeServiceFingerprinterBootstrapModule.java
│ │ │ ├── FakeVulnDetector.java
│ │ │ ├── FakeVulnDetector2.java
│ │ │ ├── FakeVulnDetectorBootstrapModule.java
│ │ │ ├── FakeVulnDetectorBootstrapModule2.java
│ │ │ ├── FakeVulnDetectorBootstrapModuleEmpty.java
│ │ │ └── FakeVulnDetectorEmpty.java
│ │ └── resources/
│ │ └── com/
│ │ └── google/
│ │ └── tsunami/
│ │ └── plugin/
│ │ └── payload/
│ │ └── payload_definitions.yaml
│ └── test/
│ └── java/
│ └── com/
│ └── google/
│ └── tsunami/
│ └── plugin/
│ ├── PluginDefinitionTest.java
│ ├── PluginExecutorImplTest.java
│ ├── PluginLoadingModuleTest.java
│ ├── PluginManagerTest.java
│ ├── PluginServiceClientTest.java
│ ├── RemoteVulnDetectorImplTest.java
│ ├── RemoteVulnDetectorLoadingModuleTest.java
│ ├── TcsClientTest.java
│ └── payload/
│ ├── PayloadGeneratorModuleTest.java
│ ├── PayloadGeneratorWithCallbackServerTest.java
│ ├── PayloadGeneratorWithoutCallbackServerTest.java
│ ├── PayloadSecretGeneratorTest.java
│ └── PayloadTest.java
├── plugin_server/
│ └── py/
│ ├── common/
│ │ ├── data/
│ │ │ ├── network_endpoint_utils.py
│ │ │ ├── network_endpoint_utils_test.py
│ │ │ ├── network_service_utils.py
│ │ │ └── network_service_utils_test.py
│ │ └── net/
│ │ └── http/
│ │ ├── host_resolver_http_adapter.py
│ │ ├── host_resolver_http_adapter_test.py
│ │ ├── http_client.py
│ │ ├── http_header_fields.py
│ │ ├── http_header_fields_test.py
│ │ ├── http_headers.py
│ │ ├── http_headers_test.py
│ │ ├── http_method.py
│ │ ├── http_request.py
│ │ ├── http_request_test.py
│ │ ├── http_response.py
│ │ ├── http_response_test.py
│ │ ├── http_status.py
│ │ ├── http_status_test.py
│ │ ├── requests_http_client.py
│ │ └── requests_http_client_test.py
│ ├── plugin/
│ │ ├── payload/
│ │ │ ├── payload.py
│ │ │ ├── payload_generator.py
│ │ │ ├── payload_generator_test.py
│ │ │ ├── payload_generator_test_helper.py
│ │ │ ├── payload_secret_generator.py
│ │ │ ├── payload_secret_generator_test.py
│ │ │ ├── payload_test.py
│ │ │ ├── payload_utility.py
│ │ │ ├── payload_utility_test.py
│ │ │ └── validator.py
│ │ ├── tcs_client.py
│ │ └── tcs_client_test.py
│ ├── plugin_server.py
│ ├── plugin_service.py
│ ├── plugin_service_test.py
│ ├── requirements.in
│ ├── requirements.txt
│ └── tsunami_plugin.py
├── proto/
│ ├── build.gradle
│ ├── detection.proto
│ ├── go/
│ │ ├── detection_go_proto/
│ │ │ └── detection.pb.go
│ │ ├── network_go_proto/
│ │ │ └── network.pb.go
│ │ ├── network_service_go_proto/
│ │ │ └── network_service.pb.go
│ │ ├── payload_generator_go_proto/
│ │ │ └── payload_generator.pb.go
│ │ ├── plugin_representation_go_proto/
│ │ │ └── plugin_representation.pb.go
│ │ ├── plugin_service_go_proto/
│ │ │ └── plugin_service.pb.go
│ │ ├── reconnaissance_go_proto/
│ │ │ └── reconnaissance.pb.go
│ │ ├── scan_results_go_proto/
│ │ │ └── scan_results.pb.go
│ │ ├── scan_target_go_proto/
│ │ │ └── scan_target.pb.go
│ │ ├── software_go_proto/
│ │ │ └── software.pb.go
│ │ ├── vulnerability_go_proto/
│ │ │ └── vulnerability.pb.go
│ │ └── web_crawl_go_proto/
│ │ └── web_crawl.pb.go
│ ├── network.proto
│ ├── network_service.proto
│ ├── payload_generator.proto
│ ├── plugin_representation.proto
│ ├── plugin_service.proto
│ ├── reconnaissance.proto
│ ├── scan_results.proto
│ ├── scan_target.proto
│ ├── software.proto
│ ├── tsunami_go_proto/
│ │ ├── detection.pb.go
│ │ ├── network.pb.go
│ │ ├── network_service.pb.go
│ │ ├── payload_generator.pb.go
│ │ ├── plugin_representation.pb.go
│ │ ├── plugin_service.pb.go
│ │ ├── reconnaissance.pb.go
│ │ ├── scan_results.pb.go
│ │ ├── scan_target.pb.go
│ │ ├── software.pb.go
│ │ ├── vulnerability.pb.go
│ │ └── web_crawl.pb.go
│ ├── vulnerability.proto
│ └── web_crawl.proto
├── settings.gradle
├── tsunami.yaml
├── tsunami_tcs.yaml
└── workflow/
├── README.md
├── build.gradle
└── src/
├── main/
│ └── java/
│ └── com/
│ └── google/
│ └── tsunami/
│ └── workflow/
│ ├── AdvisoriesWorkflow.java
│ ├── DefaultScanningWorkflow.java
│ ├── ExecutionStage.java
│ ├── ExecutionTracer.java
│ └── ScanningWorkflowException.java
└── test/
└── java/
└── com/
└── google/
└── tsunami/
└── workflow/
├── DefaultScanningWorkflowTest.java
└── ExecutionTracerTest.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Git-related files
.gitattributes
.gitignore
.git/
# IDE files
.idea/
.vscode/
# Build cache
.gradle/
# Documentation
docs/
LICENSE
README.md
# Miscellaneous
quick_start.sh
Dockerfile
.dockerignore
================================================
FILE: .gitattributes
================================================
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
================================================
FILE: .github/workflows/core-build.yml
================================================
name: core-build
on:
pull_request:
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: google/tsunami-scanner-core
jobs:
build-image:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: core.Dockerfile
push: false
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/core-push.yml
================================================
name: core-push
on:
push:
branches:
- master
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: google/tsunami-scanner-core
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: core.Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
================================================
FILE: .github/workflows/devel-push.yml
================================================
name: devel-push
on:
schedule:
- cron: "0 */4 * * *"
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: google/tsunami-scanner-devel
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: devel.Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
================================================
FILE: .github/workflows/full-push.yml
================================================
name: full-push
on:
schedule:
- cron: "0 */4 * * *"
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: google/tsunami-scanner-full
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
file: full.Dockerfile
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
================================================
FILE: .gitignore
================================================
# Gradle
build
gradle.properties
.gradle
local.properties
out
# IntelliJ IDEA
.idea
*.iml
*.ipr
*.iws
classes
# Eclipse
.classpath
.factorypath
.project
.settings
bin
eclipsebin
# OS X
.DS_Store
# Emacs
*~
\#*\#
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Tsunami

Tsunami is a general purpose network security scanner with an extensible plugin
system for detecting high severity vulnerabilities with high confidence.
To learn more about Tsunami, visit our
[documentation](https://google.github.io/tsunami-security-scanner/).
Tsunami relies heavily on its plugin system to provide basic scanning
capabilities. All publicly available Tsunami plugins are hosted in a separate
[google/tsunami-security-scanner-plugins](https://github.com/google/tsunami-security-scanner-plugins)
repository.
## Quick start
Please see the documentation on how to
[build and run Tsunami](https://google.github.io/tsunami-security-scanner/howto/howto)
## Contributing
Read how to
[contribute to Tsunami](https://google.github.io/tsunami-security-scanner/contribute/).
## License
Tsunami is released under the [Apache 2.0 license](LICENSE).
```
Copyright 2025 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
## Disclaimers
Tsunami is not an official Google product.
================================================
FILE: build.gradle
================================================
// Current gradle version 6.5.
plugins {
id 'net.ltgt.errorprone' apply false version "4.2.0"
id "com.gradleup.shadow" version "8.3.6"
}
subprojects {
apply plugin: 'java'
apply plugin: 'maven-publish'
apply plugin: 'idea'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'com.gradleup.shadow'
group = 'com.google.tsunami'
version = '0.1.1-SNAPSHOT' // Current Tsunami version
repositories {
maven { // The google mirror is less flaky than mavenCentral()
url 'https://maven-central.storage-download.googleapis.com/repos/central/data/'
}
mavenCentral()
mavenLocal()
}
if (rootProject.properties.get('errorProne', true)) {
dependencies {
errorprone "com.google.errorprone:error_prone_core:2.38.0"
errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1'
}
// Disable ErrorProne for all generated codes.
tasks.withType(JavaCompile).configureEach {
options.errorprone.disableWarningsInGeneratedCode = false
options.errorprone.excludedPaths = '.*/build/generated/.*'
}
} else {
// Disable Error Prone
allprojects {
afterEvaluate { project ->
project.tasks.withType(JavaCompile) {
options.errorprone.enabled = false
}
}
}
}
plugins.withId('java') {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
java.withJavadocJar()
java.withSourcesJar()
jar.manifest {
attributes('Implementation-Title': name,
'Implementation-Version': version,
'Built-By': System.getProperty('user.name'),
'Built-JDK': System.getProperty('java.version'),
'Source-Compatibility': sourceCompatibility,
'Target-Compatibility': targetCompatibility)
}
// Log stacktrace to console when test fails.
test {
testLogging {
exceptionFormat = 'full'
showExceptions true
showCauses true
showStackTraces true
}
maxHeapSize = '1500m'
}
}
plugins.withId('maven-publish') {
shadowJar {
archiveClassifier = null
}
}
}
================================================
FILE: common/README.md
================================================
# Tsunami Common Libraries
## Overview
This module provides a set of common libraries and utilities for Tsunami
Security Scanner.
================================================
FILE: common/build.gradle
================================================
description = 'Tsunami: Common'
dependencies {
implementation project(':tsunami-proto')
implementation "com.beust:jcommander:1.48"
implementation "com.google.auto.value:auto-value-annotations:1.11.0"
implementation "com.google.cloud:google-cloud-storage:1.103.1"
implementation "com.google.code.gson:gson:2.10.1"
implementation "com.google.flogger:flogger-system-backend:0.9"
implementation "com.google.flogger:flogger:0.9"
implementation "com.google.flogger:google-extensions:0.9"
implementation "com.google.guava:guava:33.0.0-jre"
implementation "com.google.inject:guice:6.0.0"
implementation "com.google.inject.extensions:guice-assistedinject:6.0.0"
implementation "com.google.truth:truth:1.4.4"
implementation "com.squareup.okhttp3:okhttp:3.12.0"
implementation "io.github.classgraph:classgraph:4.8.65"
implementation "org.yaml:snakeyaml:1.26"
runtimeOnly "com.mysql:mysql-connector-j:8.0.33"
runtimeOnly "org.apache.hive:hive-jdbc:4.0.1"
runtimeOnly "org.postgresql:postgresql:42.6.0"
annotationProcessor "com.google.auto.value:auto-value:1.10.4"
testAnnotationProcessor "com.google.auto.value:auto-value:1.10.4"
testImplementation "com.google.guava:guava-testlib:33.0.0-jre"
testImplementation "com.google.truth:truth:1.4.4"
testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4"
testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4"
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-core:5.18.0"
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/ErrorCode.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common;
/** Error codes for Tsunami scanner executions. */
public enum ErrorCode {
CONFIG_ERROR,
PLUGIN_EXECUTION_ERROR,
WORKFLOW_ERROR,
LANGUAGE_SERVER_ERROR,
UNKNOWN;
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/TsunamiException.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
/** Base exception definition of all Tsunami execution errors. */
public class TsunamiException extends RuntimeException {
private final ErrorCode errorCode;
public TsunamiException() {
this(ErrorCode.UNKNOWN);
}
public TsunamiException(ErrorCode errorCode) {
this(errorCode, null);
}
public TsunamiException(ErrorCode errorCode, String message) {
this(errorCode, message, null);
}
public TsunamiException(ErrorCode errorCode, String message, Throwable cause) {
super(buildExceptionMessage(checkNotNull(errorCode), message), cause);
this.errorCode = errorCode;
}
private static String buildExceptionMessage(ErrorCode errorCode, String message) {
StringBuilder exceptionMessageBuilder = new StringBuilder();
exceptionMessageBuilder.append("(Tsunami error ").append(errorCode).append(")");
if (!Strings.isNullOrEmpty(message)) {
exceptionMessageBuilder.append(": ").append(message);
}
return exceptionMessageBuilder.toString();
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/cli/CliOption.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.cli;
/**
* A marker interface for a subset of command line options used in Tsunami modules.
*
*
Client should ALWAYS mark its options with this interface so that they can be identified by
* {@link io.github.classgraph.ClassGraph}. All implementations of {@link CliOption} should provide
* a no argument constructor or omit constructors completely.
*/
public interface CliOption {
/**
* Performs additional validation logic across options defined in the same {@link CliOption}.
*
*
If validation failed, simply throw a {@link com.beust.jcommander.ParameterException}.
*/
void validate();
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/cli/CliOptionsModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.cli;
import static com.google.common.base.Preconditions.checkNotNull;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.inject.AbstractModule;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
import java.lang.reflect.Constructor;
/**
* A Guice module that parses CLI arguments for all {@link CliOption} implementations at runtime.
*
*
This module relies on the {@link io.github.classgraph.ClassGraph} scan results to identify all
* {@link CliOption} implementations at runtime. Each implementation is bound to a singleton object
* of that impl and registered to JCommander for CLI parsing.
*/
public final class CliOptionsModule extends AbstractModule {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final String CLI_OPTION_INTERFACE = "com.google.tsunami.common.cli.CliOption";
private final ScanResult scanResult;
private final String[] args;
private final JCommander jCommander;
public CliOptionsModule(ScanResult scanResult, String programName, String[] args) {
this.scanResult = checkNotNull(scanResult);
this.args = checkNotNull(args);
this.jCommander = new JCommander();
jCommander.setProgramName(programName);
}
@Override
protected void configure() {
// For each CliOption installed at runtime, bind a singleton instance and register the instance
// to JCommander for parsing.
ImmutableList.Builder cliOptions = ImmutableList.builder();
for (ClassInfo classInfo :
scanResult
.getClassesImplementing(CLI_OPTION_INTERFACE)
.filter(classInfo -> !classInfo.isInterface())) {
logger.atInfo().log("Found CliOption: %s", classInfo.getName());
CliOption cliOption = bindCliOption(classInfo.loadClass(CliOption.class));
jCommander.addObject(cliOption);
cliOptions.add(cliOption);
}
// Parse command arguments or die.
try {
jCommander.parse(args);
cliOptions.build().forEach(CliOption::validate);
} catch (ParameterException e) {
jCommander.usage();
throw e;
}
}
private T bindCliOption(Class cliOptionClass) {
try {
Constructor cliOptionCtor = cliOptionClass.getDeclaredConstructor();
// Always create an instance of the CliOption regardless of scope.
cliOptionCtor.setAccessible(true);
T cliOption = cliOptionCtor.newInstance();
bind(cliOptionClass).toInstance(cliOption);
return cliOption;
} catch (ReflectiveOperationException e) {
throw new AssertionError(
String.format(
"CliOption '%s' must be constructable via a no-argument constructor",
cliOptionClass.getTypeName()),
e);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutionThreadPool.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
/** Annotates the thread pool to use for executing native commands. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandExecutionThreadPool {}
================================================
FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutor.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.flogger.GoogleLogger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import javax.annotation.Nullable;
/** Helper class that handles running a command line and collecting output and errors. */
// TODO(b/145315535): reimplement this class so that it is:
// 1. guice injectable in order to hide Executor interface.
// 2. unit testable to prevent actually executing commands in test.
public class CommandExecutor {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final Joiner COMMAND_ARGS_JOINER = Joiner.on(" ");
private final ProcessBuilder processBuilder;
private final String[] args;
private Process process;
@Nullable private String output;
@Nullable private String error;
public CommandExecutor(String... args) {
this.args = checkNotNull(args);
this.processBuilder = new ProcessBuilder(args);
}
/*
* Executes the command and uses a {@link ThreadPoolExecutor} to collect output and error.
*
* This is a convenience method for testing purposes only as the executor is not shared and
* therefore defeats the purpose of having a cached thread pool.
*/
@VisibleForTesting
Process execute() throws IOException, InterruptedException, ExecutionException {
// Nmap is a long running process and the collectStream method is a blocking method.
// By default CompletableFuture uses ForkJoinPool, which is for suitable short
// non-blocking operations.
Executor executor = Executors.newCachedThreadPool();
return execute(executor);
}
/**
* Starts the command and uses the passed executor to collect output and error streams.
*
*
IMPORTANT: The stream collection uses an IO blocking method and the passed executor must be
* well suited for the task. {@link ThreadPoolExecutor} is a viable option.
*
* @param executor The executor to collect output and error streams.
* @return Started {@link Process} object.
* @throws IOException if an I/O error occurs when starting the command executing process.
* @throws InterruptedException if interrupted while waiting for the command's output.
* @throws ExecutionException if the command execution failed.
*/
public Process execute(Executor executor)
throws IOException, InterruptedException, ExecutionException {
logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args));
process = processBuilder.start();
output =
CompletableFuture.supplyAsync(() -> collectStream(process.getInputStream()), executor)
.get();
error =
CompletableFuture.supplyAsync(() -> collectStream(process.getErrorStream()), executor)
.get();
return process;
}
/**
* Starts the command and asynchronously collect output and error.
*
* @return Started {@link Process} object.
* @throws IOException if an I/O error occurs when starting the command executing process.
* @throws InterruptedException if interrupted while waiting for the command's output.
* @throws ExecutionException if the command execution failed.
*/
public Process executeAsync() throws IOException, InterruptedException, ExecutionException {
logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args));
process = processBuilder.inheritIO().start();
return process;
}
/**
* Starts the command without collecting output and error streams.
*
* @return Started {@link Process} object.
* @throws IOException if an I/O error occurs when starting the command executing process.
* @throws InterruptedException if interrupted while starting the command executing process.
* @throws ExecutionException if the command execution failed.
*/
public Process executeWithNoStreamCollection()
throws IOException, InterruptedException, ExecutionException {
logger.atInfo().log("Executing the following command: '%s'", COMMAND_ARGS_JOINER.join(args));
process = processBuilder.start();
return process;
}
@Nullable
public String getOutput() {
return output;
}
@Nullable
public String getError() {
return error;
}
private static String collectStream(InputStream stream) {
StringBuilder stringBuilder = new StringBuilder();
try {
String output;
BufferedReader streamReader = new BufferedReader(new InputStreamReader(stream, UTF_8));
while ((output = streamReader.readLine()) != null) {
stringBuilder.append(output);
stringBuilder.append("\n");
}
} catch (IOException e) {
logger.atWarning().withCause(e).log("Error collecting output stream from command execution.");
}
return stringBuilder.toString();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutorFactory.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
/** Utility class to simplify the creation and testing of {@link CommandExecutor} instances. */
public class CommandExecutorFactory {
private static CommandExecutor instance;
/**
* Sets an executor instance that will be returned by all future calls to {@link
* CommandExecutorFactory#create(String...)}
*
* @param executor The {@link CommandExecutor} returned by this factory.
*/
public static void setInstance(CommandExecutor executor) {
instance = executor;
}
/**
* Creates a new {@link CommandExecutor} if none is set.
*
* @param args List of arguments to pass to the newly created {@link CommandExecutor}.
* @return the {@link CommandExecutor} instance created by this factory.
*/
public static CommandExecutor create(String... args) {
if (instance == null) {
return new CommandExecutor(args);
}
return instance;
}
private CommandExecutorFactory() {}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/command/CommandExecutorModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
import com.google.inject.AbstractModule;
import com.google.tsunami.common.concurrent.ThreadPoolModule;
/** Installs dependencies used by {@link CommandExecutor}. */
public class CommandExecutorModule extends AbstractModule {
@Override
protected void configure() {
install(
new ThreadPoolModule.Builder()
.setName("CommandExecutor")
.setCoreSize(4)
.setMaxSize(8)
.setQueueCapacity(32)
.setDaemon(true)
.setPriority(Thread.NORM_PRIORITY)
.setAnnotation(CommandExecutionThreadPool.class)
.build());
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/concurrent/BaseThreadPoolModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import java.lang.annotation.Annotation;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
import java.util.concurrent.TimeUnit;
import javax.inject.Provider;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* The base module for binding a thread pool.
*
*
This module is essentially a thin wrapper around {@link ThreadFactoryBuilder} and the
* corresponding {@link ExecutorService} families. Based on the intended usage, it is expected that
* subclasses of this module should provides bindings to a concrete thread pool implementation of
* {@link ExecutorService}. This base module wraps the actual {@link ExecutorService} implementation
* in order to support {@link com.google.common.util.concurrent.ListenableFuture} usage in the code
* base.
*
* @param The expected thread pool implementation, must be a subclass of {@link
* ListeningExecutorService}.
*/
abstract class BaseThreadPoolModule
extends AbstractModule {
private final ThreadFactory factory;
private final int maxSize;
private final int coreSize;
private final long keepAliveSeconds;
private final @Nullable Duration shutdownDelay;
private final Key key;
private final Class executorServiceTypeClass;
private final RejectedExecutionHandler rejectedExecutionHandler;
BaseThreadPoolModule(BaseThreadPoolModuleBuilder builder) {
checkNotNull(builder);
this.factory = builder.factoryBuilder.build();
this.maxSize = builder.maxSize;
this.coreSize = builder.coreSize;
this.keepAliveSeconds = builder.keepAliveSeconds;
this.shutdownDelay = builder.daemon ? builder.shutdownDelay : null;
this.key = builder.key;
this.executorServiceTypeClass = builder.executorServiceTypeClass;
this.rejectedExecutionHandler = builder.rejectedExecutionHandler;
}
@Override
protected final void configure() {
configureThreadPool(key);
}
/** Subclasses should override this method for providing Guice bindings. */
abstract void configureThreadPool(Key key);
/** Base {@link Provider} implementation for providing the target thread pool. */
abstract class BaseThreadPoolProvider implements Provider {
abstract ExecutorService createThreadPool(
int coreSize,
int maxSize,
long keepAliveSeconds,
ThreadFactory factory,
RejectedExecutionHandler rejectedExecutionHandler);
@Override
public final ExecutorServiceT get() {
ExecutorService service =
createThreadPool(coreSize, maxSize, keepAliveSeconds, factory, rejectedExecutionHandler);
if (shutdownDelay != null) {
MoreExecutors.addDelayedShutdownHook(
service, shutdownDelay.toMillis(), TimeUnit.MILLISECONDS);
}
return executorServiceTypeClass.cast(MoreExecutors.listeningDecorator(service));
}
}
/** Base Builder for {@link BaseThreadPoolModule}. */
abstract static class BaseThreadPoolModuleBuilder<
ExecutorServiceT extends ListeningExecutorService,
BuilderImplT extends BaseThreadPoolModuleBuilder> {
protected final ThreadFactoryBuilder factoryBuilder = new ThreadFactoryBuilder();
protected String name;
protected int maxSize;
protected int coreSize;
protected long keepAliveSeconds = 60L;
protected boolean daemon;
protected Duration shutdownDelay;
protected Key key;
protected final Class executorServiceTypeClass;
protected RejectedExecutionHandler rejectedExecutionHandler = new AbortPolicy();
BaseThreadPoolModuleBuilder(Class executorServiceTypeClass) {
this.executorServiceTypeClass = checkNotNull(executorServiceTypeClass);
}
abstract BuilderImplT self();
/**
* Sets the name used to name the threads; automatically suffixed with "-%s"to incorporate the
* thread number
*
* @param name the name of the thread pool.
* @return the Builder instance itself.
*/
public BuilderImplT setName(String name) {
checkArgument(!Strings.isNullOrEmpty(name), "Name should not be empty");
this.name = name;
return self();
}
/**
* Sets the maximum number of threads allowed in the pool; value should be positive.
*
* @param maxSize the maximum number of threads allowed in this thread pool.
* @return the Builder instance itself.
*/
BuilderImplT setMaxSize(int maxSize) {
checkArgument(maxSize > 0, "Max thread pool size should be positive.");
this.maxSize = maxSize;
return self();
}
/**
* Sets the number of threads to keep in the pool.
*
* @param coreSize the minimum number of threads to keep alive in this thread pool.
* @return the Builder instance itself.
*/
BuilderImplT setCoreSize(int coreSize) {
checkArgument(coreSize >= 0, "The core pool size should be non-negative.");
this.coreSize = coreSize;
return self();
}
/**
* Sets the keep alive time in seconds for the threads not in core pool.
*
* @param keepAliveSeconds the maximum number of seconds an idle thread in this pool can keep
* alive before being terminated.
* @return the Builder instance itself.
*/
public BuilderImplT setKeepAliveSeconds(long keepAliveSeconds) {
checkArgument(keepAliveSeconds >= 0, "The keep alive time should be non-negative.");
this.keepAliveSeconds = keepAliveSeconds;
return self();
}
/**
* Sets whether or not new threads created by the pool will be daemon threads.*
*
* @param daemon whether threads created in this pool are daemon threads.
* @return the Builder instance itself.
*/
public BuilderImplT setDaemon(boolean daemon) {
factoryBuilder.setDaemon(daemon);
this.daemon = daemon;
return self();
}
/**
* Sets how long the JVM should wait to exit for daemon threads to complete.
*
*
This has no effect if the pool does not use daemon threads.
*
* @param shutdownDelay the delay enforced during the thread pool shutdown.
* @return the Builder instance itself.
*/
public BuilderImplT setDelayedShutdown(Duration shutdownDelay) {
this.shutdownDelay = checkNotNull(shutdownDelay);
return self();
}
/**
* Sets the priority for threads created by the pool.
*
* @param priority the priority of the threads created by this pool.
* @return the Builder instance itself.
*/
public BuilderImplT setPriority(int priority) {
factoryBuilder.setPriority(priority);
return self();
}
/**
* Sets the binding annotation.
*
* @param annotation the Guice binding annotation for this thread pool.
* @return the Builder instance itself.
*/
public BuilderImplT setAnnotation(Annotation annotation) {
key = Key.get(executorServiceTypeClass, checkNotNull(annotation));
return self();
}
/**
* Sets the binding annotation.
*
* @param annotationClass the Guice binding annotation class for this thread pool.
* @return the Builder instance itself.
*/
public BuilderImplT setAnnotation(Class extends Annotation> annotationClass) {
key = Key.get(executorServiceTypeClass, checkNotNull(annotationClass));
return self();
}
/**
* Sets the handler to use when thread execution is blocked due to thread bounds and queue
* capacities are reached.
*
*
By default, {@link AbortPolicy} is used for rejected execution, which throws the {@link
* java.util.concurrent.RejectedExecutionException}.
*
* @param rejectedExecutionHandler A handler for tasks that cannot be executed by this thread
* pool.
* @return the Builder instance itself.
*/
public BuilderImplT setRejectedExecutionHandler(
RejectedExecutionHandler rejectedExecutionHandler) {
this.rejectedExecutionHandler = checkNotNull(rejectedExecutionHandler);
return self();
}
final void validateAll() {
checkState(!Strings.isNullOrEmpty(name), "Name is required.");
checkState(
maxSize > 0,
"Max thread pool size must be positive. Did you forget setting maximum thread pool size"
+ " by calling setMaxSize?");
checkState(
coreSize <= maxSize, "Thread pool core size should be less than or equal to max size.");
checkState(key != null, "Annotation is required.");
validate();
}
abstract void validate();
public final AbstractModule build() {
validateAll();
return newModule();
}
abstract AbstractModule newModule();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/concurrent/ScheduledThreadPoolModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.inject.Key;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import javax.inject.Singleton;
/**
* A helper module for binding a scheduled thread pool. The module will bind a {@link
* ScheduledExecutorService} and a {@link ListeningScheduledExecutorService} to a singleton thread
* pool that is annotated with the annotation passed to the builder.
*/
public final class ScheduledThreadPoolModule
extends BaseThreadPoolModule {
ScheduledThreadPoolModule(Builder builder) {
super(builder);
}
@Override
void configureThreadPool(Key key) {
bind(key.ofType(ScheduledExecutorService.class)).to(key);
bind(key).toProvider(new ScheduledThreadPoolProvider()).in(Singleton.class);
}
private final class ScheduledThreadPoolProvider extends BaseThreadPoolProvider {
@Override
ScheduledThreadPoolExecutor createThreadPool(
int coreSize,
int maxSize,
long keepAliveSeconds,
ThreadFactory factory,
RejectedExecutionHandler rejectedExecutionHandler) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor =
new ScheduledThreadPoolExecutor(coreSize, factory, rejectedExecutionHandler);
scheduledThreadPoolExecutor.setMaximumPoolSize(maxSize);
scheduledThreadPoolExecutor.setKeepAliveTime(keepAliveSeconds, SECONDS);
return scheduledThreadPoolExecutor;
}
}
/**
* Builder for {@link ScheduledThreadPoolModule}.
*
*
NOTE: Unlike {@link ThreadPoolModule}, {@link ScheduledThreadPoolExecutor} acts as a
* fixed-sized pool using {@code corePoolSize} threads and an unbounded queue. So this builder
* only allows users to set a fixed thread pool size.
*/
public static final class Builder
extends BaseThreadPoolModuleBuilder {
public Builder() {
super(ListeningScheduledExecutorService.class);
}
@Override
Builder self() {
return this;
}
/**
* Sets the size of the thread pool.
*
* @param size the size of the thread pool.
* @return the {@link Builder} instance itself.
*/
public Builder setSize(int size) {
checkArgument(size > 0, "Thread pool size should be positive.");
setCoreSize(size);
setMaxSize(size);
return this;
}
@Override
void validate() {}
@Override
ScheduledThreadPoolModule newModule() {
return new ScheduledThreadPoolModule(this);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/concurrent/ThreadPoolModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Key;
import com.google.inject.Singleton;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A helper module for binding a thread pool. The module will bind an {@link Executor}, {@link
* ExecutorService} and {@link ListeningExecutorService} annotated with the annotation passed to the
* builder.
*/
public final class ThreadPoolModule extends BaseThreadPoolModule {
private final BlockingQueue blockingQueue;
private ThreadPoolModule(Builder builder) {
super(checkNotNull(builder));
this.blockingQueue = builder.getBlockingQueue();
}
@Override
void configureThreadPool(Key key) {
bind(key.ofType(Executor.class)).to(key);
bind(key.ofType(ExecutorService.class)).to(key);
bind(key).toProvider(new ThreadPoolProvider()).in(Singleton.class);
}
private final class ThreadPoolProvider extends BaseThreadPoolProvider {
@Override
ThreadPoolExecutor createThreadPool(
int coreSize,
int maxSize,
long keepAliveSeconds,
ThreadFactory factory,
RejectedExecutionHandler rejectedExecutionHandler) {
return new ThreadPoolExecutor(
coreSize,
maxSize,
keepAliveSeconds,
TimeUnit.SECONDS,
blockingQueue,
factory,
rejectedExecutionHandler);
}
}
/** Builder for {@link ThreadPoolModule}. */
public static final class Builder
extends BaseThreadPoolModuleBuilder {
private int queueCapacity = Integer.MAX_VALUE;
private @Nullable BlockingQueue blockingQueue;
public Builder() {
super(ListeningExecutorService.class);
}
@Override
Builder self() {
return this;
}
/** {@inheritDoc} */
@Override
public Builder setMaxSize(int maxSize) {
return super.setMaxSize(maxSize);
}
/** {@inheritDoc} */
@Override
public Builder setCoreSize(int coreSize) {
return super.setCoreSize(coreSize);
}
/**
* Sets the queue capacity for the thread pool.
*
*
NOTE: Users should NOT specify both this value and the {@link BlockingQueue} via {@link
* #setBlockingQueue}.
*
*
By default, {@link SynchronousQueue} will be used when {@code queueCapacity} is set to
* zero. Otherwise a {@link LinkedBlockingQueue} will be used.
*
* @param queueCapacity the capacity of the task queue.
* @return the Builder instance itself.
*/
public Builder setQueueCapacity(int queueCapacity) {
checkArgument(queueCapacity >= 0, "The queue capacity should be non-negative value.");
this.queueCapacity = queueCapacity;
return this;
}
/**
* Sets the {@link BlockingQueue} to use for holding tasks before they are executed.
*
*
NOTE: Do NOT set both {@link BlockingQueue} and {@code queueCapacity}. Only use this
* method to override the default {@link BlockingQueue} choice. See comments of {@link
* #getBlockingQueue} for which {@link BlockingQueue} is used by default.
*
* @param blockingQueue a {@link BlockingQueue} used for holding tasks before executing.
* @return the Builder instance itself.
*/
public Builder setBlockingQueue(BlockingQueue blockingQueue) {
this.blockingQueue = checkNotNull(blockingQueue);
return this;
}
private BlockingQueue getBlockingQueue() {
if (blockingQueue == null) {
return queueCapacity == 0
? new SynchronousQueue<>()
: new LinkedBlockingQueue<>(queueCapacity);
}
return blockingQueue;
}
private boolean isBoundedQueue() {
return (blockingQueue == null ? queueCapacity : blockingQueue.remainingCapacity())
< Integer.MAX_VALUE;
}
@Override
void validate() {
checkState(
blockingQueue == null || queueCapacity == Integer.MAX_VALUE,
"Both custom BlockingQueue and queue capacity are specified.");
if (coreSize < maxSize) {
checkState(
isBoundedQueue(),
"Finite capacity queue should be set when the core pool size is less than max pool"
+ " size. ThreadPoolExecutor will only create new threads past core size when the"
+ " queue is full.");
}
}
@Override
ThreadPoolModule newModule() {
return new ThreadPoolModule(this);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/ConfigException.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import com.google.tsunami.common.ErrorCode;
import com.google.tsunami.common.TsunamiException;
/** Exception when handling Tsunami configs. */
public class ConfigException extends TsunamiException {
public ConfigException(String message) {
super(ErrorCode.CONFIG_ERROR, message);
}
public ConfigException(String message, Throwable cause) {
super(ErrorCode.CONFIG_ERROR, message, cause);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/ConfigLoader.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
/** Config loader interface that load Tsunami configs from certain data sources. */
public interface ConfigLoader {
/**
* Load a {@link TsunamiConfig} object from certain data source.
*
* @return the loaded {@link TsunamiConfig} object.
*/
TsunamiConfig loadConfig();
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/ConfigModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.flogger.GoogleLogger;
import com.google.inject.AbstractModule;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
/**
* A Guice module that binds all Tsunami config objects at runtime.
*
*
This module relies on the {@link io.github.classgraph.ClassGraph} scan results to identify all
* Tsunami config objects annotated by the {@link
* com.google.tsunami.common.config.annotations.ConfigProperties} annotation. Each config class is
* bound to a singleton object whose fields are populated from the Tsunami config file.
*/
public final class ConfigModule extends AbstractModule {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final String CONFIG_PROPERTIES_ANNOTATION =
"com.google.tsunami.common.config.annotations.ConfigProperties";
private final ScanResult scanResult;
private final TsunamiConfig tsunamiConfig;
public ConfigModule(ScanResult scanResult, TsunamiConfig tsunamiConfig) {
this.scanResult = checkNotNull(scanResult);
this.tsunamiConfig = checkNotNull(tsunamiConfig);
}
@Override
protected void configure() {
bind(TsunamiConfig.class).toInstance(tsunamiConfig);
for (ClassInfo configClass :
scanResult
.getClassesWithAnnotation(CONFIG_PROPERTIES_ANNOTATION)
.filter(classInfo -> !classInfo.isAbstract())) {
logger.atInfo().log("Found Tsunami config class: %s", configClass.getName());
bindConfigClass(getConfigPrefix(configClass), configClass.loadClass());
}
}
private void bindConfigClass(String configPrefix, Class configClass) {
T configObject = tsunamiConfig.getConfig(configPrefix, configClass);
bind(configClass).toInstance(configObject);
}
private static String getConfigPrefix(ClassInfo configClass) {
Object configPrefix =
configClass
.getAnnotationInfo(CONFIG_PROPERTIES_ANNOTATION)
.getParameterValues()
.getValue("value");
if (!(configPrefix instanceof String)) {
throw new AssertionError("SHOULD NEVER HAPPEN, ConfigProperties value is not a string.");
}
return (String) configPrefix;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/TsunamiConfig.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Optional;
/** A data holder for all Tsunami config data, including config files and Java system properties. */
public final class TsunamiConfig {
private static final Converter FIELD_NAME_TO_LOWER_UNDERSCORE =
CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE);
private static final Splitter CONFIG_PATH_SPLITTER = Splitter.on('.').omitEmptyStrings();
private final ImmutableMap rawConfigData;
private TsunamiConfig(ImmutableMap rawConfigData) {
this.rawConfigData = checkNotNull(rawConfigData);
}
public ImmutableMap getRawConfigData() {
return rawConfigData;
}
public static TsunamiConfig fromYamlData(Map yamlConfig) {
return new TsunamiConfig(
yamlConfig == null ? ImmutableMap.of() : ImmutableMap.copyOf(yamlConfig));
}
public static Optional getSystemProperty(String propertyName) {
return Optional.ofNullable(getSystemProperty(propertyName, null));
}
public static String getSystemProperty(String propertyName, String def) {
return System.getProperty(propertyName, def);
}
/**
* Get a config object with the given {@code configPrefix} and bind all config values to the
* requested {@code clazz}.
*
*
This code uses reflection to create the requested config object. The request type {@code T}
* must provide a no-argument or default constructor.
*
* @param configPrefix the prefix of the config to be read from.
* @param clazz the class of the returned config object.
* @param actual config object type.
* @return an object whose field values are filled by the config data under the given {@code
* configPrefix}.
*/
public T getConfig(String configPrefix, Class clazz) {
checkNotNull(configPrefix);
checkNotNull(clazz);
Map configValue = readConfigValue(configPrefix);
return newConfigObject(clazz, configValue);
}
@SuppressWarnings("unchecked") // We know Map key is always String from yaml file.
public ImmutableMap readConfigValue(String configPrefix) {
Map retrievedData = rawConfigData;
// Config prefixes are dot separated words list, e.g. example.config.prefix.
for (String configKey : CONFIG_PATH_SPLITTER.split(configPrefix)) {
// Requested data not found under configPrefix.
if (!retrievedData.containsKey(configKey)) {
return ImmutableMap.of();
}
Object configData = retrievedData.get(configKey);
if (!(configData instanceof Map)) {
throw new ConfigException(
String.format(
"Unexpected data type for config '%s', expected '%s', got '%s'",
configKey, Map.class, configData.getClass()));
}
retrievedData = (Map) configData;
}
return ImmutableMap.copyOf(retrievedData);
}
private static T newConfigObject(Class clazz, Map configValue) {
try {
Constructor configObjectCtor = clazz.getDeclaredConstructor();
// Always create an instance of the config data regardless of scope.
configObjectCtor.setAccessible(true);
T configObject = configObjectCtor.newInstance();
// Fill each field of the configObject from configValue using the field name as key.
for (Field field : clazz.getDeclaredFields()) {
String fieldName = field.getName();
if (configValue.containsKey(fieldName)
|| configValue.containsKey(FIELD_NAME_TO_LOWER_UNDERSCORE.convert(fieldName))) {
Object fieldValue =
Optional.ofNullable(configValue.get(fieldName))
.orElse(configValue.get(FIELD_NAME_TO_LOWER_UNDERSCORE.convert(fieldName)));
field.setAccessible(true);
field.set(configObject, fieldValue);
}
}
return configObject;
} catch (ReflectiveOperationException e) {
// This is bad. Config objects cannot be created or config value cannot be assigned to the
// field, we throw assertion error and fail the execution.
throw new AssertionError(
String.format(
"Unable to create new instance of '%s' using config value '%s'", clazz, configValue),
e);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/YamlConfigLoader.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.GoogleLogger;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Map;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
/** A {@link ConfigLoader} implementation that loads Tsunami configs from YAML file. */
public final class YamlConfigLoader implements ConfigLoader {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final String DEFAULT_CONFIG_FILE = "tsunami.yaml";
@Override
public TsunamiConfig loadConfig() {
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
Map rawYamlData = yaml.load(configFileReader());
return TsunamiConfig.fromYamlData(rawYamlData);
}
private static Reader configFileReader() {
String configFile =
TsunamiConfig.getSystemProperty("tsunami.config.location").orElse(DEFAULT_CONFIG_FILE);
try {
return Files.newReader(new File(configFile), UTF_8);
} catch (FileNotFoundException e) {
logger.atWarning().log(
"Unable to read config file '%s', default to empty config.", configFile);
return new StringReader("");
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/config/annotations/ConfigProperties.java
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation for marking a Tsunami config object that can be initialized from external config
* files, e.g. a {@code .yaml} file.
*
*
This annotation is required for any config object in order for Tsunami initialization logic to
* identify and automatically populate config properties.
*
* Example usage:
*
*
{@code
* {@literal @}ConfigProperties("example.config.location")})
* public class ExampleConfig {
* // ...
* }
* }
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ConfigProperties {
/**
* The required prefix of the properties that should be bound to the annotated object.
*
*
A valid prefix is defined as dot separated words list (e.g. "plugin.example.abc"). Each dot
* separated segment represents a section within the config file. For example, given a YAML file
*
*
*
* value {@code "plugin.example.abc"} will select {@code fieldA} and {@code fieldB} for config
* binding for the annotated class.
*
* @return the prefix of the config properties.
*/
String value();
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/data/NetworkEndpointUtils.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* For any utility update, please consider if Python's network endpoint utils
* (plugin_server/py/common/data/network_endpoint_utils.py) also needs the modification.
*/
package com.google.tsunami.common.data;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.net.HostAndPort;
import com.google.common.net.InetAddresses;
import com.google.tsunami.proto.AddressFamily;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.IpAddress;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.Port;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
/** Static utility methods pertaining to {@link NetworkEndpoint} proto buffer. */
public final class NetworkEndpointUtils {
public static final int MAX_PORT_NUMBER = 65535;
private NetworkEndpointUtils() {}
public static boolean hasIpAddress(NetworkEndpoint networkEndpoint) {
return networkEndpoint.getType().equals(NetworkEndpoint.Type.IP)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_PORT)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT);
}
public static boolean hasHostname(NetworkEndpoint networkEndpoint) {
return networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME_PORT)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT);
}
public static boolean hasPort(NetworkEndpoint networkEndpoint) {
return networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_PORT)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.HOSTNAME_PORT)
|| networkEndpoint.getType().equals(NetworkEndpoint.Type.IP_HOSTNAME_PORT);
}
public static boolean isIpV6Endpoint(NetworkEndpoint networkEndpoint) {
return hasIpAddress(networkEndpoint)
&& networkEndpoint.getIpAddress().getAddressFamily().equals(AddressFamily.IPV6);
}
/**
* Converts the given {@link NetworkEndpoint} to its uri authority representation.
*
*
For example:
*
*
*
ip_v4 = "1.2.3.4" -> uri = "1.2.3.4"
*
ip_v6 = "3ffe::1" -> uri = "[3ffe::1]"
*
host = "localhost" -> url = "localhost"
*
ip_v4 = "1.2.3.4", port = 8888 -> uri = "1.2.3.4:8888"
*
ip_v6 = "3ffe::1", port = 8888 -> uri = "[3ffe::1]:8888"
*
*
* @param networkEndpoint the {@link NetworkEndpoint} instance to be converted.
* @return the URI authority converted from the {@link NetworkEndpoint} instance.
*/
public static String toUriAuthority(NetworkEndpoint networkEndpoint) {
return toHostAndPort(networkEndpoint).toString();
}
public static HostAndPort toHostAndPort(NetworkEndpoint networkEndpoint) {
switch (networkEndpoint.getType()) {
case IP:
return HostAndPort.fromHost(networkEndpoint.getIpAddress().getAddress());
case IP_PORT:
return HostAndPort.fromParts(
networkEndpoint.getIpAddress().getAddress(), networkEndpoint.getPort().getPortNumber());
case HOSTNAME:
case IP_HOSTNAME:
return HostAndPort.fromHost(networkEndpoint.getHostname().getName());
case HOSTNAME_PORT:
case IP_HOSTNAME_PORT:
return HostAndPort.fromParts(
networkEndpoint.getHostname().getName(), networkEndpoint.getPort().getPortNumber());
case UNRECOGNIZED:
case TYPE_UNSPECIFIED:
throw new AssertionError("Type for NetworkEndpoint must be specified.");
}
throw new AssertionError(
String.format(
"Should never happen. Unchecked NetworkEndpoint type: %s", networkEndpoint.getType()));
}
/**
* Creates a {@link NetworkEndpoint} proto buffer object from the given ip address.
*
* @param ipAddress the IP address of the network endpoint.
* @return the created {@link NetworkEndpoint} instance from the given IP address.
*/
public static NetworkEndpoint forIp(String ipAddress) {
checkArgument(InetAddresses.isInetAddress(ipAddress), "'%s' is not an IP address.", ipAddress);
return NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(ipAddressFamily(ipAddress))
.setAddress(ipAddress))
.build();
}
/**
* Creates a {@link NetworkEndpoint} proto buffer object from the given ip address and port.
*
* @param ipAddress the IP address of the network endpoint.
* @param port the port number of the network endpoint
* @return the created {@link NetworkEndpoint} instance from the given IP and port.
*/
public static NetworkEndpoint forIpAndPort(String ipAddress, int port) {
checkArgument(InetAddresses.isInetAddress(ipAddress), "'%s' is not an IP address.", ipAddress);
checkArgument(
0 <= port && port <= MAX_PORT_NUMBER,
"Port out of range. Expected [0, %s], actual %s.",
MAX_PORT_NUMBER,
port);
return forIp(ipAddress).toBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
}
/**
* Creates a {@link NetworkEndpoint} proto buffer object from the given hostname.
*
* @param hostname the hostname of the network endpoint
* @return the created {@link NetworkEndpoint} instance from the hostname.
*/
public static NetworkEndpoint forHostname(String hostname) {
checkArgument(
!InetAddresses.isInetAddress(hostname), "Expected hostname, got IP address '%s'", hostname);
return NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME)
.setHostname(Hostname.newBuilder().setName(hostname))
.build();
}
/**
* Creates a {@link NetworkEndpoint} proto buffer object from the given ip address and hostname.
*
* @param hostname the hostname of the network endpoint
* @param ipAddress the IP address of the network endpoint.
* @return the created {@link NetworkEndpoint} instance from the IP address and hostname.
*/
public static NetworkEndpoint forIpAndHostname(String ipAddress, String hostname) {
return forIp(ipAddress).toBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME)
.setHostname(Hostname.newBuilder().setName(hostname))
.build();
}
/**
* Creates a {@link NetworkEndpoint} proto buffer object from the given hostname and port.
*
* @param hostname the hostname of the network endpoint
* @param port the port number of the network endpoint.
* @return the created {@link NetworkEndpoint} instance from the hostname and port.
*/
public static NetworkEndpoint forHostnameAndPort(String hostname, int port) {
checkArgument(
0 <= port && port <= MAX_PORT_NUMBER,
"Port out of range. Expected [0, %s], actual %s.",
MAX_PORT_NUMBER,
port);
return forHostname(hostname).toBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
}
/**
* Returns a {@link NetworkEndpoint} proto buffer object from the given ip address, hostname and
* port.
*
* @param ipAddress the IP address of the network endpoint.
* @param hostname the hostname of the network endpoint
* @param port the port number of the network endpoint.
* @return the created {@link NetworkEndpoint} instance from the parameters.
*/
public static NetworkEndpoint forIpHostnameAndPort(String ipAddress, String hostname, int port) {
checkArgument(
0 <= port && port <= MAX_PORT_NUMBER,
"Port out of range. Expected [0, %s], actual %s.",
MAX_PORT_NUMBER,
port);
return forIpAndHostname(ipAddress, hostname).toBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
}
/**
* Returns a {@link NetworkEndpoint} proto buffer object from the given {@code networkEndpoint}
* and port. The {@code networkEndpoint} parameter cannot contain any port information, otherwise
* {@link IllegalArgumentException} is thrown.
*
* @param networkEndpoint the source {@link NetworkEndpoint} instance without the port number
* @param port the port number of the network endpoint.
* @return the {@link NetworkEndpoint} instance from the parameters.
*/
public static NetworkEndpoint forNetworkEndpointAndPort(
NetworkEndpoint networkEndpoint, int port) {
checkNotNull(networkEndpoint);
checkArgument(
0 <= port && port <= MAX_PORT_NUMBER,
"Port out of range. Expected [0, %s], actual %s.",
MAX_PORT_NUMBER,
port);
switch (networkEndpoint.getType()) {
case IP:
return networkEndpoint.toBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
case HOSTNAME:
return networkEndpoint.toBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
case IP_HOSTNAME:
return networkEndpoint.toBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.build();
case IP_PORT:
case HOSTNAME_PORT:
case IP_HOSTNAME_PORT:
case UNRECOGNIZED:
case TYPE_UNSPECIFIED:
throw new IllegalArgumentException("Invalid NetworkEndpoint type.");
}
throw new AssertionError(
String.format(
"Should never happen. Unchecked NetworkEndpoint type: %s", networkEndpoint.getType()));
}
public static AddressFamily ipAddressFamily(String ipAddress) {
InetAddress inetAddress = InetAddresses.forString(ipAddress);
if (inetAddress instanceof Inet4Address) {
return AddressFamily.IPV4;
} else if (inetAddress instanceof Inet6Address) {
return AddressFamily.IPV6;
} else {
throw new AssertionError(String.format("Unknown IP address family for IP '%s'", ipAddress));
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* For any utility update, please consider if Python's network service utils
* (plugin_server/py/common/data/network_service_utils.py) also needs the modification.
*/
package com.google.tsunami.common.data;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.tsunami.proto.AddressFamily;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.IpAddress;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.NetworkService;
import com.google.tsunami.proto.Port;
import com.google.tsunami.proto.ServiceContext;
import com.google.tsunami.proto.TransportProtocol;
import com.google.tsunami.proto.WebServiceContext;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Optional;
/** Static utility methods pertaining to {@link NetworkService} proto buffer. */
public final class NetworkServiceUtils {
// Service names are those described in [RFC6335].
private static final ImmutableMap IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME =
ImmutableMap.builder()
.put("http", true)
.put("http-alt", true) // Some http server are identified as this rather than "http".
.put("http-proxy", true)
.put("https", false)
.put("radan-http", true) // Port 8088, Hadoop Yarn web UI identified as this.
.put("ssl/http", false)
.put("ssl/https", false)
.put("ssl/http-proxy", false)
.put("ssl/tungsten-https", false) // Port 9443, WSO2 Identity Server & WSO2 API Manager.
.put("ssl/wso2esb-console", false) // Port 9444, WSO2 Identity Server Analytics.
.build();
private NetworkServiceUtils() {}
public static boolean isWebService(Optional serviceName) {
return serviceName.isPresent()
&& IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.containsKey(
Ascii.toLowerCase(serviceName.get()));
}
public static boolean isWebService(NetworkService networkService) {
checkNotNull(networkService);
// A web-service is a service that is either flagged as http by nmap or one that supports at
// least one HTTP method.
return (networkService.getSupportedHttpMethodsCount() > 0)
|| isWebService(Optional.of(networkService.getServiceName()));
}
public static boolean isPlainHttp(NetworkService networkService) {
checkNotNull(networkService);
var isWebService = isWebService(networkService);
var isKnownServiceName = IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.containsKey(
Ascii.toLowerCase(networkService.getServiceName()));
var doesNotSupportAnySslVersion = networkService.getSupportedSslVersionsCount() == 0;
if (!isKnownServiceName) {
return isWebService && doesNotSupportAnySslVersion;
}
var isKnownPlainHttpService =
IS_PLAIN_HTTP_BY_KNOWN_WEB_SERVICE_NAME.getOrDefault(
Ascii.toLowerCase(networkService.getServiceName()), false);
return isKnownPlainHttpService && doesNotSupportAnySslVersion;
}
public static String getServiceName(NetworkService networkService) {
if (isWebService(networkService) && networkService.hasSoftware()) {
return Ascii.toLowerCase(networkService.getSoftware().getName());
}
return Ascii.toLowerCase(networkService.getServiceName());
}
public static String getWebServiceName(NetworkService networkService) {
if (isWebService(networkService)
&& networkService.getServiceContext().getWebServiceContext().hasSoftware()) {
return Ascii.toLowerCase(
networkService.getServiceContext().getWebServiceContext().getSoftware().getName());
}
return Ascii.toLowerCase(networkService.getServiceName());
}
public static NetworkService buildUriNetworkService(String uriString) {
try {
URI uri = new URI(uriString);
NetworkEndpoint uriEndPoint = buildUriNetworkEndPoint(uri);
return NetworkService.newBuilder()
.setNetworkEndpoint(uriEndPoint)
.setTransportProtocol(TransportProtocol.TCP)
.setServiceName(uri.getScheme())
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot(uri.getPath())))
.build();
} catch (URISyntaxException exception) {
throw new AssertionError(
String.format(
"Invalid uri syntax passed as target '%s'. Error: %s", uriString, exception));
}
}
private static NetworkEndpoint buildUriNetworkEndPoint(URI uri) {
try {
String hostname = uri.getHost();
String scheme = uri.getScheme();
checkArgument(
scheme.equals("http") || scheme.equals("https"),
"Uri scheme should be one of the following: 'http', 'https'");
int port = uri.getPort();
if (port < 0) {
port = scheme.equals("http") ? 80 : 443;
}
String ipAddress = InetAddress.getByName(hostname).getHostAddress();
InetAddress inetAddress = InetAddress.getByName(uri.getHost());
checkArgument(
(inetAddress instanceof Inet4Address) || (inetAddress instanceof Inet6Address),
"Invalid address family");
AddressFamily addressFamily =
inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6;
return NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(port))
.setHostname(Hostname.newBuilder().setName(uri.getHost()))
.setIpAddress(
IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipAddress))
.build();
} catch (UnknownHostException exception) {
throw new AssertionError(
String.format("Unable to get valid host from uri. Error: %s", exception));
}
}
/**
* Build the root url for a web application service.
*
* @param networkService a web (http/https) service
* @return the root url for the web service, which always ends with a "/".
*/
public static String buildWebApplicationRootUrl(NetworkService networkService) {
checkNotNull(networkService);
if (!isWebService(networkService)) {
return "http://"
+ NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint())
+ "/";
}
String rootUrl =
(isPlainHttp(networkService) ? "http://" : "https://")
+ buildWebUriAuthority(networkService)
+ buildWebAppRootPath(networkService);
return rootUrl.endsWith("/") ? rootUrl : rootUrl + "/";
}
private static String buildWebAppRootPath(NetworkService networkService) {
String rootPath =
networkService.getServiceContext().hasWebServiceContext()
? networkService.getServiceContext().getWebServiceContext().getApplicationRoot()
: "/";
if (!rootPath.startsWith("/")) {
rootPath = "/" + rootPath;
}
return rootPath;
}
private static String buildWebUriAuthority(NetworkService networkService) {
String uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
// Remove default ports of the protocol.
boolean isPlainHttp = isPlainHttp(networkService);
if (isPlainHttp && uriAuthority.endsWith(":80")) {
uriAuthority = uriAuthority.substring(0, uriAuthority.lastIndexOf(":80"));
}
if (!isPlainHttp && uriAuthority.endsWith(":443")) {
uriAuthority = uriAuthority.substring(0, uriAuthority.lastIndexOf(":443"));
}
return uriAuthority;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/Archiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** An {@link Archiver} archives the given data to some data storage. */
public interface Archiver {
/**
* Archives the {@code data} associated with the given {@code name}.
*
* @param name the name that will be associated with the data
* @param data the data to be archived in byte array format
* @return whether the given data is archived successfully.
*/
@CanIgnoreReturnValue
boolean archive(String name, byte[] data);
/**
* Archives the {@code data} associated with the given {@code name}. By default, this method
* encodes the {@link CharSequence} {@code data} into a sequence of bytes using {@code UTF_8}
* {@link java.nio.charset.StandardCharsets} and calls the {@link #archive(String, byte[])}
* method.
*
* @param name the name that will be associated with the data
* @param data the data to be archived in {@link CharSequence} format
* @return whether the given data is archived successfully.
*/
@CanIgnoreReturnValue
default boolean archive(String name, CharSequence data) {
return archive(name, checkNotNull(data).toString().getBytes(UTF_8));
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.validators.PositiveInteger;
import com.google.cloud.WriteChannel;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.common.flogger.GoogleLogger;
import com.google.inject.assistedinject.Assisted;
import com.google.tsunami.common.cli.CliOption;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
/** An {@link Archiver} implementation that archives data into Google Cloud Storage. */
public class GoogleCloudStorageArchiver implements Archiver {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
// For sanity-checking and to parse out the bucket name and object id.
// See https://cloud.google.com/storage/docs/bucket-naming
public static final Pattern GS_URL_PATTERN = Pattern.compile("gs://([^/]{3,63})/(.*)");
private final Options options;
private final Storage storage;
/** All command line options for {@link GoogleCloudStorageArchiver}. */
@Parameters(separators = "=")
public static final class Options implements CliOption {
@Parameter(
names = "--gcs-archiver-chunk-size-in-bytes",
description = "The size of the data chunk when GCS archiver uploads data to Cloud Storage.",
validateWith = PositiveInteger.class)
int chunkSizeInBytes = 1_000;
@Parameter(
names = "--gcs-archiver-chunk-upload-threshold-in-bytes",
description = "The default data size threshold in bytes to enable chunk upload to GCS.",
validateWith = PositiveInteger.class)
int chunkUploadThresholdInBytes = 1_000_000;
@Override
public void validate() {}
}
@Inject
public GoogleCloudStorageArchiver(Options options, @Assisted Storage storage) {
this.options = checkNotNull(options);
this.storage = checkNotNull(storage);
}
private static BlobInfo parseBlobInfo(String gcsUrl) {
Matcher matcher = GS_URL_PATTERN.matcher(gcsUrl);
checkArgument(matcher.matches(), "Invalid GCS URL: '%s'", gcsUrl);
String bucketName = matcher.group(1);
String objectName = matcher.group(2);
return BlobInfo.newBuilder(bucketName, objectName).build();
}
@Override
public boolean archive(String gcsUrl, byte[] data) {
BlobInfo blobInfo = parseBlobInfo(gcsUrl);
if (data.length <= options.chunkUploadThresholdInBytes) {
// Create the blob in one request.
logger.atInfo().log("Archiving data to GCS at '%s' in one request.", gcsUrl);
storage.create(blobInfo, data);
return true;
}
// When content is large (1MB or more) it is recommended to write it in chunks via the blob's
// channel writer.
logger.atInfo().log(
"Content is larger than threshold, archiving data to GCS at '%s' in chunks.", gcsUrl);
try (WriteChannel writer = storage.writer(blobInfo)) {
for (int chunkOffset = 0;
chunkOffset < data.length;
chunkOffset += options.chunkSizeInBytes) {
int chunkSize = Math.min(data.length - chunkOffset, options.chunkSizeInBytes);
writer.write(ByteBuffer.wrap(data, chunkOffset, chunkSize));
}
return true;
} catch (IOException e) {
logger.atSevere().withCause(e).log("Unable to archving data to GCS at '%s'.", gcsUrl);
return false;
}
}
/** The factory of {@link GoogleCloudStorageArchiver} types for usage with assisted injection. */
// TODO(b/145315535): consider wrap the Storage API into a client library. Current implementation
// is not easily testable.
public interface Factory {
GoogleCloudStorageArchiver create(Storage storage);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiverModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
/** Installs {@link GoogleCloudStorageArchiver}. */
public class GoogleCloudStorageArchiverModule extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder().build(GoogleCloudStorageArchiver.Factory.class));
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/RawFileArchiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.common.flogger.GoogleLogger;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
/** An {@link Archiver} implementation that archives data into file systems as raw files. */
public class RawFileArchiver implements Archiver {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
@Override
public boolean archive(String fileName, byte[] data) {
checkArgument(!Strings.isNullOrEmpty(fileName));
checkNotNull(data);
try {
logger.atInfo().log("Archiving data to file system with filename '%s'.", fileName);
Files.asByteSink(new File(fileName)).write(data);
return true;
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed archiving data to file '%s'.", fileName);
return false;
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeArchiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving.testing;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.Maps;
import com.google.tsunami.common.io.archiving.Archiver;
import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
/** An implementation of {@link Archiver} that stores data in memory for testing purposes. */
public final class FakeArchiver implements Archiver {
private final Map archivedByteArrayData = Maps.newHashMap();
private final Map archivedCharSequenceData = Maps.newHashMap();
private boolean shouldFail = false;
@Override
public boolean archive(String name, byte[] data) {
if (shouldFail) {
return false;
}
archivedByteArrayData.put(name, data);
return true;
}
@Override
public boolean archive(String name, CharSequence data) {
if (shouldFail) {
return false;
}
archivedCharSequenceData.put(name, data);
return true;
}
public void failArchival() {
this.shouldFail = true;
}
public byte[] getStoredByteArrays(String name) {
if (!archivedByteArrayData.containsKey(name)) {
throw new NoSuchElementException(String.format("'%s' not found in FakeArchiver", name));
}
return archivedByteArrayData.get(name);
}
public CharSequence getStoredCharSequence(String name) {
if (!archivedCharSequenceData.containsKey(name)) {
throw new NoSuchElementException(String.format("'%s' not found in FakeArchiver", name));
}
return archivedCharSequenceData.get(name);
}
public void assertNoByteArraysStored() {
assertThat(archivedByteArrayData).isEmpty();
}
public void assertNoCharSequencesStored() {
assertThat(archivedCharSequenceData).isEmpty();
}
public void assertNoDataStored() {
assertNoByteArraysStored();
assertNoCharSequencesStored();
}
public void assertByteArraysStored(Map expectedData) {
assertThat(archivedByteArrayData).containsExactlyEntriesIn(expectedData);
}
public void assertByteArraysStoredForNames(Set expectedNames) {
assertThat(archivedByteArrayData.keySet()).containsExactlyElementsIn(expectedNames);
}
public void assertByteArraysStoredWithValues(Collection expectedValues) {
assertThat(archivedByteArrayData.values()).containsExactlyElementsIn(expectedValues);
}
public void assertCharSequencesStored(Map expectedData) {
assertThat(archivedCharSequenceData).containsExactlyEntriesIn(expectedData);
}
public void assertCharSequencesStoredForNames(Set expectedNames) {
assertThat(archivedCharSequenceData.keySet()).containsExactlyElementsIn(expectedNames);
}
public void assertCharSequencesStoredWithValues(Collection expectedValues) {
assertThat(archivedCharSequenceData.values()).containsExactlyElementsIn(expectedValues);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeGoogleCloudStorageArchivers.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving.testing;
import com.google.cloud.storage.Storage;
import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
/** A collection of fake {@link GoogleCloudStorageArchiver} created by {@link FakeFactory}. */
public final class FakeGoogleCloudStorageArchivers {
private final Map delegatedArchivers = new HashMap<>();
public void assertNoDataStored() {
for (FakeArchiver delegate : delegatedArchivers.values()) {
delegate.assertNoDataStored();
}
}
/**
* Get the byte array data stored in {@code storage} at {@code gcsUrl}.
*
* @param storage the instance of the GCS storage.
* @param gcsUrl the URL to the GCS storage object.
* @return the content of the GCS storage object in byte array format.
*/
public byte[] getStoredByteArrays(Storage storage, String gcsUrl) {
if (!delegatedArchivers.containsKey(storage)) {
throw new NoSuchElementException(String.format("Storage '%s' not found", storage));
}
return delegatedArchivers.get(storage).getStoredByteArrays(gcsUrl);
}
/**
* Get the {@link CharSequence} data stored in {@code storage} at {@code gcsUrl}.
*
* @param storage the instance of the GCS storage.
* @param gcsUrl the URL to the GCS storage object.
* @return the content of the GCS storage object in {@link CharSequence} format.
*/
public CharSequence getStoredCharSequence(Storage storage, String gcsUrl) {
if (!delegatedArchivers.containsKey(storage)) {
throw new NoSuchElementException(String.format("Storage '%s' not found", storage));
}
return delegatedArchivers.get(storage).getStoredCharSequence(gcsUrl);
}
final class FakeGoogleCloudStorageArchiver extends GoogleCloudStorageArchiver {
private final Storage storage;
private FakeGoogleCloudStorageArchiver(Storage storage) {
super(new Options(), storage);
this.storage = storage;
}
@Override
public boolean archive(String gcsUrl, byte[] data) {
FakeArchiver fakeArchiver =
delegatedArchivers.computeIfAbsent(storage, unused -> new FakeArchiver());
return fakeArchiver.archive(gcsUrl, data);
}
@Override
public boolean archive(String gcsUrl, CharSequence data) {
FakeArchiver fakeArchiver =
delegatedArchivers.computeIfAbsent(storage, unused -> new FakeArchiver());
return fakeArchiver.archive(gcsUrl, data);
}
}
final class FakeFactory implements GoogleCloudStorageArchiver.Factory {
@Override
public GoogleCloudStorageArchiver create(Storage storage) {
return new FakeGoogleCloudStorageArchiver(storage);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeGoogleCloudStorageArchiversModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving.testing;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver;
import javax.inject.Singleton;
/** Installs fake factory for {@link GoogleCloudStorageArchiver}. */
public final class FakeGoogleCloudStorageArchiversModule extends AbstractModule {
@Provides
@Singleton
GoogleCloudStorageArchiver.Factory provideGoogleCloudStorageArchiverFactory(
FakeGoogleCloudStorageArchivers fakeGoogleCloudStorageArchivers) {
return fakeGoogleCloudStorageArchivers.new FakeFactory();
}
@Provides
@Singleton
FakeGoogleCloudStorageArchivers provideFakeGoogleCloudStorageArchivers() {
return new FakeGoogleCloudStorageArchivers();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeRawFileArchiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving.testing;
import com.google.tsunami.common.io.archiving.RawFileArchiver;
/** A fake implementation of the {@link RawFileArchiver}. */
public final class FakeRawFileArchiver extends RawFileArchiver {
private final FakeArchiver delegate = new FakeArchiver();
@Override
public boolean archive(String fileName, byte[] data) {
return delegate.archive(fileName, data);
}
@Override
public boolean archive(String fileName, CharSequence data) {
return delegate.archive(fileName, data);
}
public byte[] getStoredByteArrays(String fileName) {
return delegate.getStoredByteArrays(fileName);
}
public CharSequence getStoredCharSequence(String fileName) {
return delegate.getStoredCharSequence(fileName);
}
public void assertNoDataStored() {
delegate.assertNoDataStored();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/io/archiving/testing/FakeRawFileArchiverModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving.testing;
import com.google.inject.AbstractModule;
import com.google.tsunami.common.io.archiving.RawFileArchiver;
import javax.inject.Singleton;
/** Installs {@link FakeRawFileArchiver}. */
public final class FakeRawFileArchiverModule extends AbstractModule {
@Override
protected void configure() {
// This is intentional to create 2 separate bindings. One is for FakeRawFileArchiver itself,
// which always injects as a singleton. The other one links the binding for RawFileArchiver to
// FakeRawFileArchiver so that the FakeRawFileArchiver singleton instance is injected to
// RawFileArchiver. This way the classes on the inheritance chain always get the same instance.
//
// This is useful in unit test. Test cases now are able to get the same injected instance as the
// code under test.
bind(FakeRawFileArchiver.class).in(Singleton.class);
bind(RawFileArchiver.class).to(FakeRawFileArchiver.class);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/FuzzingUtils.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.joining;
import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.tsunami.common.net.http.HttpRequest;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** Fuzzing utilities for HTTP request properties. */
public final class FuzzingUtils {
/* TODO(b/251480660): Refactor to generic fuzzing library. */
/**
* Fuzz GET parameters by replacing values with the provided payload. If no GET parameter is
* found, add a new parameter called {@code defaultParameter}.
*/
public static ImmutableList fuzzGetParametersWithDefaultParameter(
HttpRequest request, String payload, String defaultParameter) {
return fuzzGetParameters(request, payload, Optional.of(defaultParameter), ImmutableSet.of());
}
/**
* Fuzz GET parameters by replacing values with the provided payload. Payloads are expected to
* represent paths. If encountered, file extesions and path prefixes are kept and provided via
* additional exploit requests. If no GET parameter is found, return an empty list.
*/
public static ImmutableList fuzzGetParametersExpectingPathValues(
HttpRequest request, String payload) {
return fuzzGetParameters(
request, payload, Optional.empty(), ImmutableSet.of(FuzzingModifier.FUZZING_PATHS));
}
/**
* Fuzz GET parameters by replacing values with the provided payload. If no GET parameter is
* found, return an empty list.
*/
public static ImmutableList fuzzGetParameters(HttpRequest request, String payload) {
return fuzzGetParameters(request, payload, Optional.empty(), ImmutableSet.of());
}
private static ImmutableList fuzzGetParameters(
HttpRequest request,
String payload,
Optional defaultParameter,
ImmutableSet modifiers) {
URI parsedUrl = URI.create(request.url());
ImmutableList queryParams = parseQuery(parsedUrl.getQuery());
if (queryParams.isEmpty() && defaultParameter.isPresent()) {
return ImmutableList.of(
request.toBuilder()
.setUrl(
assembleUrlWithQueries(
parsedUrl,
ImmutableList.of(HttpQueryParameter.create(defaultParameter.get(), payload))))
.build());
}
return fuzzParams(queryParams, payload, modifiers).stream()
.map(fuzzedParams -> assembleUrlWithQueries(parsedUrl, fuzzedParams))
.map(fuzzedUrl -> request.toBuilder().setUrl(fuzzedUrl).build())
.collect(toImmutableList());
}
private static ImmutableList setFuzzedParams(
ImmutableList params, int index, String payload) {
List paramsWithPayload = new ArrayList<>(params);
paramsWithPayload.set(index, HttpQueryParameter.create(params.get(index).name(), payload));
return ImmutableList.copyOf(paramsWithPayload);
}
private static void fuzzParamsWithExtendedPathPayloads(
ImmutableSet.Builder> builder,
ImmutableList params,
int index,
String payload) {
int dotLocation = params.get(index).value().lastIndexOf('.');
if (dotLocation != -1) {
builder.add(
setFuzzedParams(
params, index, payload + "%00" + params.get(index).value().substring(dotLocation)));
}
int slashLocation = params.get(index).value().lastIndexOf('/');
if (slashLocation != -1) {
builder.add(
setFuzzedParams(
params, index, params.get(index).value().substring(0, slashLocation + 1) + payload));
}
if (dotLocation != -1 && slashLocation != -1 && slashLocation < dotLocation) {
builder.add(
setFuzzedParams(
params,
index,
params.get(index).value().substring(0, slashLocation + 1)
+ payload
+ "%00"
+ params.get(index).value().substring(dotLocation)));
}
}
private static ImmutableSet> fuzzParams(
ImmutableList params,
String payload,
ImmutableSet modifiers) {
ImmutableSet.Builder> fuzzedParamsBuilder =
ImmutableSet.builder();
for (int i = 0; i < params.size(); i++) {
fuzzedParamsBuilder.add(setFuzzedParams(params, i, payload));
if (modifiers.contains(FuzzingModifier.FUZZING_PATHS)) {
fuzzParamsWithExtendedPathPayloads(fuzzedParamsBuilder, params, i, payload);
}
}
return fuzzedParamsBuilder.build();
}
public static ImmutableList parseQuery(String query) {
if (isNullOrEmpty(query)) {
return ImmutableList.of();
}
ImmutableList.Builder queryParamsBuilder = ImmutableList.builder();
for (String param : Splitter.on('&').split(query)) {
int equalPosition = param.indexOf("=");
if (equalPosition > -1) {
String name = param.substring(0, equalPosition);
String value = param.substring(equalPosition + 1);
queryParamsBuilder.add(HttpQueryParameter.create(name, value));
} else {
queryParamsBuilder.add(HttpQueryParameter.create(param, ""));
}
}
return queryParamsBuilder.build();
}
private static String assembleUrlWithQueries(
URI parsedUrl, ImmutableList params) {
String query = assembleQueryParams(params);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(parsedUrl.getScheme()).append("://").append(parsedUrl.getRawAuthority());
if (!isNullOrEmpty(parsedUrl.getRawPath())) {
urlBuilder.append(parsedUrl.getRawPath());
}
if (!isNullOrEmpty(query)) {
urlBuilder.append('?').append(query);
}
if (!isNullOrEmpty(parsedUrl.getRawFragment())) {
urlBuilder.append('#').append(parsedUrl.getRawFragment());
}
return urlBuilder.toString();
}
private static String assembleQueryParams(ImmutableList params) {
return params.stream()
.map(param -> String.format("%s=%s", param.name(), param.value()))
.collect(joining("&"));
}
/** URL Query parameter name and value pair. */
@AutoValue
public abstract static class HttpQueryParameter {
public abstract String name();
public abstract String value();
public static HttpQueryParameter create(String name, String value) {
return new AutoValue_FuzzingUtils_HttpQueryParameter(name, value);
}
}
enum FuzzingModifier {
FUZZING_PATHS;
}
private FuzzingUtils() {}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/UrlUtils.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import okhttp3.HttpUrl;
/** Utilities for dealing with URLs. */
public final class UrlUtils {
private static final Joiner PATH_JOINER = Joiner.on("/");
private static final Pattern SLASH_PREFIX_PATTERN = Pattern.compile("^/+");
private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/+$");
/**
* Enumerates all sub-paths for a given URL. All query parameters and fragments are removed.
*
*
For example:
*
*
*
given "http://localhost/", it returns ["http://localhost/"]
*
given "http://localhost/a/b/", it returns
* ["http://localhost/", "http://localhost/a/", "http://localhost/a/b/"]
*
*
* @param url the URL to be enumerated.
* @return all sub-paths URLs for the given URL.
*/
public static ImmutableSet allSubPaths(String url) {
return allSubPaths(HttpUrl.parse(url));
}
/**
* Enumerates all sub-paths for a given URL. All query parameters and fragments are removed.
*
*
For example:
*
*
*
given "http://localhost/", it returns ["http://localhost/"]
*
given "http://localhost/a/b/", it returns
* ["http://localhost/", "http://localhost/a/", "http://localhost/a/b/"]
*
*
* @param url the URL to be enumerated.
* @return all sub-paths URLs for the given URL.
*/
public static ImmutableSet allSubPaths(HttpUrl url) {
if (url == null) {
return ImmutableSet.of();
}
// Url at root.
List pathSegments = url.encodedPathSegments();
if (pathSegments.size() == 1 && pathSegments.get(0).isEmpty()) {
return ImmutableSet.of(url.newBuilder().query(null).fragment(null).build());
}
// Url has sub-paths.
ImmutableSet.Builder allSubUrlsBuilder = ImmutableSet.builder();
for (int pathEnd = 0; pathEnd <= pathSegments.size(); pathEnd++) {
List subPathSegments = Lists.newArrayList(pathSegments.subList(0, pathEnd));
// Ensure sub-path has leading slash.
if (subPathSegments.isEmpty() || !subPathSegments.get(0).isEmpty()) {
subPathSegments.add(0, "");
}
// Ensure sub-path has trailing slash.
if (subPathSegments.size() == 1 || !Iterables.getLast(subPathSegments).isEmpty()) {
subPathSegments.add("");
}
allSubUrlsBuilder.add(
url.newBuilder()
.encodedPath(PATH_JOINER.join(subPathSegments))
.query(null)
.fragment(null)
.build());
}
return allSubUrlsBuilder.build();
}
/**
* Removes the leading slashes of a URL path.
*
* @param path the URL path to be transformed.
* @return a URL path without leading slash.
*/
public static String removeLeadingSlashes(String path) {
return SLASH_PREFIX_PATTERN.matcher(path).replaceFirst("");
}
/**
* Removes the trailing slashes of a URL path.
*
* @param path the URL path to be transformed.
* @return a URL path without leading slash.
*/
public static String removeTrailingSlashes(String path) {
return TRAILING_SLASH_PATTERN.matcher(path).replaceFirst("");
}
/**
* Encodes the given String using URL-encoding.
*
* @param raw the raw String to be encoded.
* @return the URL-encoded version of the provided String if it was valid UTF-8.
*/
public static Optional urlEncode(String raw) {
try {
return Optional.of(URLEncoder.encode(raw, UTF_8.toString()));
} catch (UnsupportedEncodingException e) {
return Optional.empty();
}
}
private UrlUtils() {}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/db/ConnectionProvider.java
================================================
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/** A client library that communicates with different databases via jdbc. */
public class ConnectionProvider implements ConnectionProviderInterface {
public ConnectionProvider() {}
@Override
public Connection getConnection(String url, String user, String password) throws SQLException {
return DriverManager.getConnection(url, user, password);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/db/ConnectionProviderInterface.java
================================================
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.db;
import java.sql.Connection;
import java.sql.SQLException;
/** A client interface that communicates with different databases. */
public interface ConnectionProviderInterface {
public Connection getConnection(String url, String user, String password) throws SQLException;
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClient.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import java.time.Duration;
import org.checkerframework.checker.nullness.qual.Nullable;
/** A client library that communicates with remote servers via the HTTP protocol. */
public abstract class HttpClient {
public static final String TSUNAMI_USER_AGENT = "TsunamiSecurityScanner";
/**
* Gets log id.
*
* @return log id string.
*/
public abstract String getLogId();
/**
* NOTE: This is a temporary hack to workaround OkHttp's hardcoded URL canonicalization algorithm.
* We should rewrite the entire library using a more flexible backend.
*
*
Sends the given HTTP request as is, blocking until full response is received.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
public abstract HttpResponse sendAsIs(HttpRequest httpRequest) throws IOException;
/**
* Sends the given HTTP request using this client, blocking until full response is received.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
public abstract HttpResponse send(HttpRequest httpRequest) throws IOException;
/**
* Sends the given HTTP request using this client blocking until full response is received. If
* {@code networkService} is not null, the host header is set according to the service's header
* field even if it resolves to a different ip.
*
* @param httpRequest the HTTP request to be sent by this client.
* @param networkService the {@link NetworkService} proto to be used for the HOST header.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
public abstract HttpResponse send(
HttpRequest httpRequest, @Nullable NetworkService networkService) throws IOException;
/**
* Sends the given HTTP request using this client asynchronously.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the future for the response to be returned from the HTTP server.
*/
public abstract ListenableFuture sendAsync(HttpRequest httpRequest);
/**
* Sends the given HTTP request using this client asynchronously. If {@code networkService} is not
* null, the host header is set according to the service's header field even if it resolves to a
* different ip.
*
* @param httpRequest the HTTP request to be sent by this client.
* @param networkService the {@link NetworkService} proto to be used for the HOST header.
* @return the future for the response to be returned from the HTTP server.
*/
public abstract ListenableFuture sendAsync(
HttpRequest httpRequest, @Nullable NetworkService networkService);
public abstract Builder modify();
/** Base builder for implementations of HttpClient */
public abstract static class Builder {
public abstract Builder setFollowRedirects(boolean followRedirects);
public abstract Builder setLogId(String logId);
public abstract Builder setConnectTimeout(Duration connectionTimeout);
public abstract T build();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientCliOptions.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.tsunami.common.cli.CliOption;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Command line argument for {@link HttpClient}. */
@Parameters(separators = "=")
public final class HttpClientCliOptions implements CliOption {
@Parameter(
names = "--http-client-trust-all-certificates",
arity = 1,
description = "Whether the HTTP client should trust all certificates on HTTPS traffic.")
public Boolean trustAllCertificates;
@Parameter(
names = "--http-client-call-timeout-seconds",
description =
"[Depreciated] Set to be the same as the timeout specified by"
+ " --http-client-connect-timeout-seconds.")
Integer callTimeoutSeconds;
@Parameter(
names = "--http-client-connect-timeout-seconds",
description =
"The timeout in seconds for new HTTP connections. See"
+ " https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/"
+ " for more details.")
Integer connectTimeoutSeconds;
@Parameter(
names = "--http-client-read-timeout-seconds",
description =
"[Depreciated] Set to be the same as the timeout specified by"
+ " --http-client-connect-timeout-seconds")
Integer readTimeoutSeconds;
@Parameter(
names = "--http-client-write-timeout-seconds",
description =
"[Depreciated] Set to be the same as the timeout specified by"
+ " --http-client-connect-timeout-seconds.")
Integer writeTimeoutSeconds;
@Parameter(
names = "--http-client-user-agent",
description = "User-Agent to use in HTTP requests.")
public String userAgent = HttpClient.TSUNAMI_USER_AGENT;
@Override
public void validate() {
validateTimeout("--http-client-call-timeout-seconds", callTimeoutSeconds);
validateTimeout("--http-client-connect-timeout-seconds", connectTimeoutSeconds);
validateTimeout("--http-client-read-timeout-seconds", readTimeoutSeconds);
validateTimeout("--http-client-write-timeout-seconds", writeTimeoutSeconds);
}
private static void validateTimeout(String flagName, @Nullable Integer value) {
if (value != null && value < 0) {
throw new ParameterException(
String.format("%s cannot be a negative number, received %d.", flagName, value));
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientConfigProperties.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import com.google.tsunami.common.config.annotations.ConfigProperties;
/** Configuration properties for {@link HttpClient}. */
@ConfigProperties("common.net.http")
public final class HttpClientConfigProperties {
/** Whether the HTTP client should trust all certificates on HTTPS traffic. */
Boolean trustAllCertificates;
/**
* The timeout in seconds for complete HTTP calls. See
* https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout/ for
* more details.
*/
Integer callTimeoutSeconds;
/**
* The timeout in seconds for new HTTP connections. See
* https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/
* for more details.
*/
Integer connectTimeoutSeconds;
/**
* The timeout in seconds for the read operations for HTTP connections. See
* https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/read-timeout/ for
* more details.
*/
Integer readTimeoutSeconds;
/**
* The timeout in seconds for the write operations for HTTP connections. See
* https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/write-timeout/ for
* more details.
*/
Integer writeTimeoutSeconds;
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpClientModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.tsunami.common.net.http.javanet.ConnectionFactory;
import com.google.tsunami.common.net.http.javanet.DefaultConnectionFactory;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
/** Guice module for installing {@link HttpClient} library. */
public final class HttpClientModule extends AbstractModule {
// This TrustManager does NOT validate certificate chains.
private static final X509TrustManager TRUST_ALL_CERTS_MANAGER =
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
// Maximum number of requests for each host (URL's host name) to execute concurrently.
private static final int OKHTTPCLIENT_MAX_REQUESTS_PER_HOST = 5;
// Maximum number of idle connections to each to keep in the pool.
private final int connectionPoolMaxIdle;
// Duration to keep the connection alive in the pool before closing it.
private final Duration connectionPoolKeepAliveDuration;
// Maximum number of requests to execute concurrently.
private final int maxRequests;
// Whether or not to follow redirect from server.
private final boolean followRedirects;
// A log ID to print in front of the logs.
private final String logId;
public HttpClientModule(Builder builder) {
checkNotNull(builder);
this.connectionPoolMaxIdle = builder.connectionPoolMaxIdle;
this.connectionPoolKeepAliveDuration = builder.connectionPoolKeepAliveDuration;
this.maxRequests = builder.maxRequests;
this.followRedirects = builder.followRedirects;
this.logId = builder.logId;
}
@Provides
@Singleton
ConnectionPool provideConnectionPool() {
return new ConnectionPool(
connectionPoolMaxIdle, connectionPoolKeepAliveDuration.toMillis(), MILLISECONDS);
}
@Provides
@Singleton
Dispatcher provideDispatcher() {
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(maxRequests);
dispatcher.setMaxRequestsPerHost(OKHTTPCLIENT_MAX_REQUESTS_PER_HOST);
return dispatcher;
}
@Provides
@Singleton
@TrustAllCertsSocketFactory
SSLSocketFactory provideTrustAllCertsSocketFactory() throws GeneralSecurityException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {TRUST_ALL_CERTS_MANAGER}, new SecureRandom());
return sslContext.getSocketFactory();
}
// Missing features:
// 1. Custom cookie handler.
@Provides
@Singleton
OkHttpClient provideOkHttpClient(
ConnectionPool connectionPool,
Dispatcher dispatcher,
@TrustAllCertsSocketFactory SSLSocketFactory trustAllCertsSocketFactory,
@TrustAllCertificates boolean trustAllCertificates,
@ConnectTimeoutSeconds int connectTimeoutSeconds) {
OkHttpClient.Builder clientBuilder =
new OkHttpClient.Builder()
.callTimeout(Duration.ofSeconds(connectTimeoutSeconds))
.connectTimeout(Duration.ofSeconds(connectTimeoutSeconds))
.readTimeout(Duration.ofSeconds(connectTimeoutSeconds))
.writeTimeout(Duration.ofSeconds(connectTimeoutSeconds))
.connectionPool(connectionPool)
.dispatcher(dispatcher)
.followRedirects(followRedirects);
if (trustAllCertificates) {
clientBuilder
.sslSocketFactory(trustAllCertsSocketFactory, TRUST_ALL_CERTS_MANAGER)
.hostnameVerifier((hostname, session) -> true);
}
return clientBuilder.build();
}
@Provides
@Singleton
HttpClient provideOkHttpHttpClient(
OkHttpClient okHttpClient,
@TrustAllCertificates boolean trustAllCertificates,
ConnectionFactory connectionFactory,
@LogId String logId,
@ConnectTimeout Duration connectTimeout,
@UserAgent String userAgent) {
return new OkHttpHttpClient(
okHttpClient, trustAllCertificates, connectionFactory, logId, connectTimeout, userAgent);
}
@Provides
@Singleton
ConnectionFactory provideJavaNetConnectionFactory(
@TrustAllCertificates boolean trustAllCertificates,
@TrustAllCertsSocketFactory SSLSocketFactory trustAllCertsSocketFactory,
@ConnectTimeoutSeconds int connectTimeoutSeconds,
@ReadTimeoutSeconds int readTimeoutSeconds) {
return new DefaultConnectionFactory(
trustAllCertificates,
trustAllCertsSocketFactory,
Duration.ofSeconds(connectTimeoutSeconds),
Duration.ofSeconds(readTimeoutSeconds));
}
@Provides
@TrustAllCertificates
boolean shouldTrustAllCertificates(
HttpClientCliOptions httpClientCliOptions,
HttpClientConfigProperties httpClientConfigProperties) {
if (httpClientCliOptions.trustAllCertificates != null) {
return httpClientCliOptions.trustAllCertificates;
}
if (httpClientConfigProperties.trustAllCertificates != null) {
return httpClientConfigProperties.trustAllCertificates;
}
return true;
}
@Provides
@LogId
String provideLogid() {
return logId;
}
@Provides
@FollowRedirects
boolean provideFollowRedirects() {
return followRedirects;
}
@Provides
@MaxRequests
int provideMaxRequests() {
return maxRequests;
}
@Provides
@CallTimeoutSeconds
int provideCallTimeoutSeconds(
HttpClientCliOptions httpClientCliOptions,
HttpClientConfigProperties httpClientConfigProperties) {
if (httpClientCliOptions.callTimeoutSeconds != null) {
return httpClientCliOptions.callTimeoutSeconds;
}
if (httpClientConfigProperties.callTimeoutSeconds != null) {
return httpClientConfigProperties.callTimeoutSeconds;
}
// Default call timeout specified in
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout/.
return 0;
}
@Provides
@ConnectTimeoutSeconds
int provideConnectTimeoutSeconds(
HttpClientCliOptions httpClientCliOptions,
HttpClientConfigProperties httpClientConfigProperties) {
if (httpClientCliOptions.connectTimeoutSeconds != null) {
return httpClientCliOptions.connectTimeoutSeconds;
}
if (httpClientConfigProperties.connectTimeoutSeconds != null) {
return httpClientConfigProperties.connectTimeoutSeconds;
}
// Default connect timeout specified in
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout/.
return 10;
}
@Provides
@ConnectTimeout
Duration provideConnectTimeout(@ConnectTimeoutSeconds int connectionTimeoutSeconds) {
return Duration.ofSeconds(connectionTimeoutSeconds);
}
@Provides
@ReadTimeoutSeconds
int provideReadTimeoutSeconds(
HttpClientCliOptions httpClientCliOptions,
HttpClientConfigProperties httpClientConfigProperties) {
if (httpClientCliOptions.readTimeoutSeconds != null) {
return httpClientCliOptions.readTimeoutSeconds;
}
if (httpClientConfigProperties.readTimeoutSeconds != null) {
return httpClientConfigProperties.readTimeoutSeconds;
}
// Default read timeout specified in
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/read-timeout/.
return 10;
}
@Provides
@WriteTimeoutSeconds
int provideWriteTimeoutSeconds(
HttpClientCliOptions httpClientCliOptions,
HttpClientConfigProperties httpClientConfigProperties) {
if (httpClientCliOptions.writeTimeoutSeconds != null) {
return httpClientCliOptions.writeTimeoutSeconds;
}
if (httpClientConfigProperties.writeTimeoutSeconds != null) {
return httpClientConfigProperties.writeTimeoutSeconds;
}
// Default write timeout specified in
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/write-timeout/.
return 10;
}
@Provides
@UserAgent
String provideUserAgent(HttpClientCliOptions httpClientCliOptions) {
if (!isNullOrEmpty(httpClientCliOptions.userAgent)) {
return httpClientCliOptions.userAgent;
}
return HttpClient.TSUNAMI_USER_AGENT;
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface TrustAllCertificates {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface TrustAllCertsSocketFactory {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface LogId {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface CallTimeoutSeconds {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface ConnectTimeoutSeconds {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface ConnectTimeout {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface ReadTimeoutSeconds {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface WriteTimeoutSeconds {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface FollowRedirects {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface MaxRequests {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@interface UserAgent {}
/** Builder for {@link HttpClientModule}. */
public static final class Builder {
private static final int DEFAULT_CONNECTION_POOL_MAX_IDLE = 5;
private static final Duration DEFAULT_CONNECTION_POOL_KEEP_ALIVE_DURATION =
Duration.ofMinutes(5);
private static final int DEFAULT_MAX_REQUESTS = 64;
private static final boolean DEFAULT_FOLLOW_REDIRECTS = true;
private static final String DEFAULT_LOG_ID = "";
private int connectionPoolMaxIdle = DEFAULT_CONNECTION_POOL_MAX_IDLE;
private Duration connectionPoolKeepAliveDuration = DEFAULT_CONNECTION_POOL_KEEP_ALIVE_DURATION;
private int maxRequests = DEFAULT_MAX_REQUESTS;
private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
private String logId = DEFAULT_LOG_ID;
/**
* Sets the maximum number of idle connections to each to keep in the pool.
*
* @param maxIdle maximum number of idel connecteds.
* @return the {@link Builder} instance itself.
*/
public Builder setConnectionPoolMaxIdle(int maxIdle) {
checkArgument(maxIdle > 0);
this.connectionPoolMaxIdle = maxIdle;
return this;
}
/**
* Sets the duration to keep the connection alive in the connection pool before closing it.
*
* @param keepAliveDuration the duration to keep the connection alive.
* @return the {@link Builder} instance itself.
*/
public Builder setConnectionPoolKeepAliveDuration(Duration keepAliveDuration) {
checkNotNull(keepAliveDuration);
checkArgument(!keepAliveDuration.isNegative());
this.connectionPoolKeepAliveDuration = keepAliveDuration;
return this;
}
/**
* Sets the maximum number of requests to execute concurrently.
*
* @param maxRequests the maximum number of concurrent requests.
* @return the {@link Builder} instance itself.
*/
public Builder setMaxRequests(int maxRequests) {
checkArgument(maxRequests > 0);
this.maxRequests = maxRequests;
return this;
}
/**
* Sets whether or not to follow redirect from server. If unset, by default redirects will be
* followed.
*
* @param followRedirects whether the HTTP client should follow redirect responses from the
* server.
* @return the {@link Builder} instance itself.
*/
public Builder setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
/**
* Sets the log ID to print in front of the logs.
*
* @param logId the log ID to print in front of the logs.
* @return the {@link Builder} instance itself.
*/
public Builder setLogId(String logId) {
this.logId = logId;
return this;
}
public HttpClientModule build() {
return new HttpClientModule(this);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpHeaders.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auto.value.AutoValue;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.Immutable;
import java.lang.reflect.Field;
import java.util.Optional;
/** Immutable HTTP headers. */
@Immutable
@AutoValue
public abstract class HttpHeaders {
private static final ImmutableBiMap LOWER_TO_KNOWN = createKnownHeaders();
private static final ImmutableSet KNOWN = LOWER_TO_KNOWN.values();
/** Canonicalize a header name. */
private static String canonicalize(String headerName) {
if (KNOWN.contains(headerName)) {
return headerName;
}
String lower = Ascii.toLowerCase(headerName);
String known = LOWER_TO_KNOWN.get(lower);
return MoreObjects.firstNonNull(known, lower);
}
private static ImmutableBiMap createKnownHeaders() {
ImmutableBiMap.Builder builder = ImmutableBiMap.builder();
addFields(builder, com.google.common.net.HttpHeaders.class);
return builder.build();
}
/**
* Loops over all of the public String fields in the given class and puts them into the BiMap
* (lower case to original string value).
*/
private static void addFields(ImmutableBiMap.Builder builder, Class> clazz) {
try {
for (Field field : clazz.getFields()) {
if (field.getType().equals(String.class)) {
String known = (String) field.get(null);
String lower = Ascii.toLowerCase(known);
builder.put(lower, known);
}
}
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}
abstract ImmutableListMultimap rawHeaders();
/**
* Gets a set of all HTTP header names.
*
* @return all HTTP header names.
*/
public ImmutableSet names() {
return rawHeaders().keySet();
}
/**
* Returns the first value for the header with the given name, or empty Optional if none exists.
*
* @param name case-insensitive header name
* @return the first value for the given header name.
*/
public Optional get(String name) {
checkNotNull(name, "Name cannot be null.");
ImmutableList values = getAll(name);
return values.isEmpty() ? Optional.empty() : Optional.of(values.get(0));
}
/**
* Returns all the values for the header with the given name. Values are in the same order they
* were added to the builder.
*
* @param name case-insensitive header name
* @return All values for the given header name.
*/
public ImmutableList getAll(String name) {
checkNotNull(name, "Name cannot be null.");
// We first check the multimap using whatever string is passed in. Usually
// this will be a constant from HttpHeaders, which is pre-canonicalized.
// Only if the lookup fails do we then canonicalize and try again.
ImmutableList values = rawHeaders().get(name);
if (!values.isEmpty()) {
return values;
}
String fixedName = canonicalize(name);
if (fixedName.equals(name)) {
return values; // Name was already canonicalized, so return the empty list.
}
return rawHeaders().get(fixedName);
}
public static Builder builder() {
return new AutoValue_HttpHeaders.Builder();
}
/** Builder for {@link HttpHeaders}. */
@AutoValue.Builder
public abstract static class Builder {
/** RFC 2616 section 4.2. */
private static final CharMatcher HEADER_NAME_MATCHER =
CharMatcher.inRange('!', '~').and(CharMatcher.isNot(':'));
/** RFC 2616 section 4.2. */
private static final CharMatcher HEADER_VALUE_MATCHER =
CharMatcher.inRange((char) 0, (char) 31) // No control characters
.or(CharMatcher.is((char) 127)) // or DEL
.negate()
.or(CharMatcher.is('\t')); // except horizontal-tab
abstract ImmutableListMultimap.Builder rawHeadersBuilder();
public Builder addHeader(String name, String value) {
checkNotNull(name, "Name cannot be null.");
checkNotNull(value, "Value cannot be null.");
checkArgument(isLegalHeaderName(name), "Illegal header name %s", name);
checkArgument(isLegalHeaderValue(value), "Illegal header value %s", value);
rawHeadersBuilder().put(canonicalize(name), value);
return this;
}
public Builder addHeader(String name, String value, boolean canonicalize) {
checkNotNull(name, "Name cannot be null.");
checkNotNull(value, "Value cannot be null.");
if (canonicalize) {
return addHeader(name, value);
} else {
rawHeadersBuilder().put(name, value);
return this;
}
}
public abstract HttpHeaders build();
private static boolean isLegalHeaderName(String str) {
return HEADER_NAME_MATCHER.matchesAllOf(str);
}
private static boolean isLegalHeaderValue(String value) {
return HEADER_VALUE_MATCHER.matchesAllOf(value);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpMethod.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
/** Represents HTTP methods. */
public enum HttpMethod {
// Add more http methods here if necessary.
GET("GET"),
HEAD("HEAD"),
POST("POST"),
PUT("PUT"),
DELETE("DELETE");
private final String string;
HttpMethod(String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpRequest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ByteString;
import java.util.Optional;
import okhttp3.HttpUrl;
/** Immutable HTTP request. */
@Immutable
@AutoValue
@AutoValue.CopyAnnotations
@SuppressWarnings("Immutable")
public abstract class HttpRequest {
public abstract HttpMethod method();
public abstract String url();
public abstract HttpHeaders headers();
public abstract Optional requestBody();
public abstract Builder toBuilder();
/**
* Creates a {@link Builder} object for configuring {@link HttpRequest}.
*
* @return a {@link Builder} instance for the {@link HttpRequest} object.
*/
public static Builder builder() {
return new AutoValue_HttpRequest.Builder();
}
/**
* Create a new HTTP GET request with the given {@code url}.
*
* @param url the url of the GET request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder get(String url) {
checkArgument(!Strings.isNullOrEmpty(url));
return builder().setMethod(HttpMethod.GET).setUrl(url);
}
/**
* Create a new HTTP GET request with the given {@code uri}.
*
* @param uri the url of the GET request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder get(HttpUrl uri) {
checkNotNull(uri);
return builder().setMethod(HttpMethod.GET).setUrl(uri);
}
/**
* Create a new HTTP HEAD request with the given {@code url}.
*
* @param url the url of the HEAD request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder head(String url) {
checkArgument(!Strings.isNullOrEmpty(url));
return builder().setMethod(HttpMethod.HEAD).setUrl(url);
}
/**
* Create a new HTTP HEAD request with the given {@code uri}.
*
* @param uri the url of the HEAD request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder head(HttpUrl uri) {
checkNotNull(uri);
return builder().setMethod(HttpMethod.HEAD).setUrl(uri);
}
/**
* Create a new HTTP POST request with the given {@code url}.
*
* @param url the url of the POST request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder post(String url) {
checkArgument(!Strings.isNullOrEmpty(url));
return builder().setMethod(HttpMethod.POST).setUrl(url);
}
/**
* Create a new HTTP POST request with the given {@code uri}.
*
* @param uri the url of the POST request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder post(HttpUrl uri) {
checkNotNull(uri);
return builder().setMethod(HttpMethod.POST).setUrl(uri);
}
/**
* Create a new HTTP PUT request with the given {@code url}.
*
* @param url the url of the PUT request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder put(String url) {
checkArgument(!Strings.isNullOrEmpty(url));
return put(HttpUrl.parse(url));
}
/**
* Create a new HTTP PUT request with the given {@code uri}.
*
* @param uri the url of the PUT request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder put(HttpUrl uri) {
checkNotNull(uri);
return builder().setMethod(HttpMethod.PUT).setUrl(uri);
}
/**
* Create a new HTTP DELETE request with the given {@code url}.
*
* @param url the url of the DELETE request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder delete(String url) {
checkArgument(!Strings.isNullOrEmpty(url));
return builder().setMethod(HttpMethod.DELETE).setUrl(url);
}
/**
* Create a new HTTP DELETE request with the given {@code uri}.
*
* @param uri the url of the DELETE request.
* @return a {@link Builder} object for configuring {@link HttpRequest}.
*/
public static Builder delete(HttpUrl uri) {
checkNotNull(uri);
return builder().setMethod(HttpMethod.DELETE).setUrl(uri);
}
/** Builder for {@link HttpRequest}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setMethod(HttpMethod method);
public abstract Builder setUrl(String url);
public Builder setUrl(HttpUrl url) {
setUrl(url.toString());
return this;
}
public abstract Builder setHeaders(HttpHeaders httpHeaders);
public abstract Builder setRequestBody(ByteString requestBody);
public abstract Builder setRequestBody(Optional requestBody);
public Builder withEmptyHeaders() {
setHeaders(HttpHeaders.builder().build());
return this;
}
abstract HttpRequest autoBuild();
public HttpRequest build() {
HttpRequest httpRequest = autoBuild();
switch (httpRequest.method()) {
case GET:
case HEAD:
checkState(
!httpRequest.requestBody().isPresent(),
"A request body is not allowed for HTTP GET/HEAD request");
break;
case POST:
case PUT:
case DELETE:
break;
}
return httpRequest;
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpResponse.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.errorprone.annotations.Immutable;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.protobuf.ByteString;
import java.util.Optional;
import okhttp3.HttpUrl;
/** Immutable HTTP response. */
@Immutable
@AutoValue
@AutoValue.CopyAnnotations
// HttpUrl is immutable even if not marked as such.
@SuppressWarnings("Immutable")
public abstract class HttpResponse {
public abstract HttpStatus status();
public abstract HttpHeaders headers();
public abstract Optional bodyBytes();
// The URL that produced this response.
// TODO(b/173574468): Provide the full redirection request not just the Url.
public abstract Optional responseUrl();
/**
* Gets the body of the HTTP response as a UTF-8 encoded String.
*
* @return HTTP response body as a Java {@link String}.
*/
@Memoized
public Optional bodyString() {
return bodyBytes().map(ByteString::toStringUtf8);
}
/**
* Tries to parse the response body as json and returns the parsing result as {@link JsonElement}.
* If parsing failed, an empty optional is returned.
*
* @return HTTP response body as a Gson {@link JsonElement} object.
*/
@Memoized
public Optional bodyJson() {
try {
return bodyString().map(JsonParser::parseString);
} catch (RuntimeException e) {
// Do best-effort parsing and ignore Json parsing errors
return Optional.empty();
}
}
/**
* Tries to determine if a given field in Json response is equal to a specific value. If parsing
* failed, {@link com.google.gson.JsonSyntaxException} or {@link IllegalStateException} will be
* thrown.
*
* @return boolean
*/
public boolean jsonFieldEqualsToValue(String fieldname, String value) {
Optional jsonPrimitive =
bodyJson()
.map(JsonElement::getAsJsonObject)
.map(object -> object.getAsJsonPrimitive(fieldname));
return jsonPrimitive.isPresent() && jsonPrimitive.get().getAsString().equals(value);
}
public static Builder builder() {
return new AutoValue_HttpResponse.Builder();
}
/** Builder for {@link HttpResponse}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setStatus(HttpStatus httpStatus);
public abstract Builder setHeaders(HttpHeaders httpHeaders);
public abstract Builder setBodyBytes(ByteString bodyBytes);
public abstract Builder setBodyBytes(Optional bodyBytes);
public abstract Builder setResponseUrl(HttpUrl url);
public abstract Builder setResponseUrl(Optional url);
public abstract HttpResponse build();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/HttpStatus.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.function.Function;
/**
* HTTP Status Codes defined in RFC 2616, RFC 6585, RFC 4918 and RFC 7538.
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
* @see http://tools.ietf.org/html/rfc6585
* @see https://tools.ietf.org/html/rfc4918
* @see https://tools.ietf.org/html/rfc7538
*/
public enum HttpStatus {
// Default
HTTP_STATUS_UNSPECIFIED(0, "Status Unspecified"),
// Informational 1xx
CONTINUE(100, "Continue"),
SWITCHING_PROTOCOLS(101, "Switching Protocols"),
// Successful 2xx
OK(200, "Ok"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"),
NO_CONTENT(204, "No Content"),
RESET_CONTENT(205, "Reset Content"),
PARTIAL_CONTENT(206, "Partial Content"),
MULTI_STATUS(207, "Multi-Status"),
// Redirection 3xx
MULTIPLE_CHOICES(300, "Multiple Choices"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
FOUND(302, "Found"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
USE_PROXY(305, "Use Proxy"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
PERMANENT_REDIRECT(308, "Permanent Redirect"),
// Client Error 4xx
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
PAYMENT_REQUIRED(402, "Payment Required"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
REQUEST_URI_TOO_LONG(414, "Request Uri Too Long"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
REQUEST_RANGE_NOT_SATISFIABLE(416, "Request Range Not Satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
LOCKED(423, "Locked"),
FAILED_DEPENDENCY(424, "Failed Dependency"),
PRECONDITION_REQUIRED(428, "Precondition Required"),
TOO_MANY_REQUESTS(429, "Too Many Requests"),
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
// Server Error 5xx
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable"),
GATEWAY_TIMEOUT(504, "Gateway Timeout"),
HTTP_VERSION_NOT_SUPPORTED(505, "Http Version Not Supported"),
INSUFFICIENT_STORAGE(507, "Insufficient Storage"),
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"),
/*
* IE returns this code for 204 due to its use of URLMon, which returns this
* code for 'Operation Aborted'. The status text is 'Unknown', the response
* headers are ''. Known to occur on IE 6 on XP through IE9 on Win7.
*/
QUIRK_IE_NO_CONTENT(1223, "Quirk IE No Content");
/** Status indexed by code. */
private static final ImmutableMap BY_CODE =
Arrays.stream(HttpStatus.values())
.collect(toImmutableMap(HttpStatus::code, Function.identity()));
/**
* Creates the {@link HttpStatus} from the given status code, or null if there is no known status
* with that code.
*
* @param code the HTTP status code.
* @return the matching {@link HttpStatus} from the given status code.
*/
public static HttpStatus fromCode(int code) {
HttpStatus status = BY_CODE.get(code);
return status == null ? HTTP_STATUS_UNSPECIFIED : status;
}
private final int code;
private final String name;
HttpStatus(int code, String name) {
this.code = code;
this.name = name;
}
public int code() {
return code;
}
public boolean isRedirect() {
switch (this) {
case MULTIPLE_CHOICES:
case MOVED_PERMANENTLY:
case FOUND:
case SEE_OTHER:
case TEMPORARY_REDIRECT:
case PERMANENT_REDIRECT:
return true;
default:
return false;
}
}
public boolean isSuccess() {
return code >= 200 && code < 300;
}
@Override
public String toString() {
return name;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/OkHttpHttpClient.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.GoogleLogger;
import com.google.common.io.ByteSource;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.ByteString;
import com.google.tsunami.common.net.http.javanet.ConnectionFactory;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Dns;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A client library that communicates with remote servers via the HTTP protocol using {@link
* OkHttpClient}.
*/
final class OkHttpHttpClient extends HttpClient {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final OkHttpClient okHttpClient;
private final boolean trustAllCertificates;
private final ConnectionFactory connectionFactory;
private final String logId;
private final Duration connectionTimeout;
private final String userAgent;
OkHttpHttpClient(
OkHttpClient okHttpClient,
boolean trustAllCertificates,
ConnectionFactory connectionFactory,
String logId,
Duration connectionTimeout,
String userAgent) {
this.okHttpClient = checkNotNull(okHttpClient);
this.trustAllCertificates = trustAllCertificates;
this.connectionFactory = checkNotNull(connectionFactory);
this.logId = logId;
this.connectionTimeout = connectionTimeout;
this.userAgent = isNullOrEmpty(userAgent) ? TSUNAMI_USER_AGENT : userAgent;
}
/**
* Gets log id.
*
* @return log id string.
*/
@Override
public String getLogId() {
return this.logId;
}
/**
* NOTE: This is a temporary hack to workaround OkHttp's hardcoded URL canonicalization algorithm.
* We should rewrite the entire library using a more flexible backend.
*
*
Sends the given HTTP request as is, blocking until full response is received.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
@Override
public HttpResponse sendAsIs(HttpRequest httpRequest) throws IOException {
HttpURLConnection connection = connectionFactory.openConnection(httpRequest.url());
connection.setRequestMethod(httpRequest.method().toString());
httpRequest.headers().names().stream()
.filter(headerName -> !Ascii.equalsIgnoreCase(headerName, USER_AGENT))
.forEach(
headerName ->
httpRequest
.headers()
.getAll(headerName)
.forEach(
headerValue -> connection.setRequestProperty(headerName, headerValue)));
connection.setRequestProperty(USER_AGENT, this.userAgent);
if (ImmutableSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)
.contains(httpRequest.method())) {
connection.setDoOutput(true);
ByteSource.wrap(httpRequest.requestBody().orElse(ByteString.EMPTY).toByteArray())
.copyTo(connection.getOutputStream());
}
int responseCode = connection.getResponseCode();
HttpHeaders.Builder responseHeadersBuilder = HttpHeaders.builder();
for (Map.Entry> headerEntry : connection.getHeaderFields().entrySet()) {
String headerName = headerEntry.getKey();
if (!isNullOrEmpty(headerName)) {
for (String headerValue : headerEntry.getValue()) {
if (!isNullOrEmpty(headerValue)) {
responseHeadersBuilder.addHeader(headerName, headerValue);
}
}
}
}
return HttpResponse.builder()
.setStatus(HttpStatus.fromCode(responseCode))
.setHeaders(responseHeadersBuilder.build())
.setBodyBytes(ByteString.readFrom(connection.getInputStream()))
.build();
}
/**
* Sends the given HTTP request using this client, blocking until full response is received.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
@Override
public HttpResponse send(HttpRequest httpRequest) throws IOException {
return send(httpRequest, null);
}
/**
* Sends the given HTTP request using this client blocking until full response is received. If
* {@code networkService} is not null, the host header is set according to the service's header
* field even if it resolves to a different ip.
*
* @param httpRequest the HTTP request to be sent by this client.
* @param networkService the {@link NetworkService} proto to be used for the HOST header.
* @return the response returned from the HTTP server.
* @throws IOException if an I/O error occurs during the HTTP request.
*/
@Override
public HttpResponse send(HttpRequest httpRequest, @Nullable NetworkService networkService)
throws IOException {
logger.atInfo().log(
"%sSending HTTP '%s' request to '%s'.", logId, httpRequest.method(), httpRequest.url());
OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService);
try (Response okHttpResponse =
callHttpClient.newCall(buildOkHttpRequest(httpRequest, this.userAgent)).execute()) {
return parseResponse(okHttpResponse);
}
}
/**
* Sends the given HTTP request using this client asynchronously.
*
* @param httpRequest the HTTP request to be sent by this client.
* @return the future for the response to be returned from the HTTP server.
*/
@Override
public ListenableFuture sendAsync(HttpRequest httpRequest) {
return sendAsync(httpRequest, null);
}
/**
* Sends the given HTTP request using this client asynchronously. If {@code networkService} is not
* null, the host header is set according to the service's header field even if it resolves to a
* different ip.
*
* @param httpRequest the HTTP request to be sent by this client.
* @param networkService the {@link NetworkService} proto to be used for the HOST header.
* @return the future for the response to be returned from the HTTP server.
*/
@Override
public ListenableFuture sendAsync(
HttpRequest httpRequest, @Nullable NetworkService networkService) {
logger.atInfo().log(
"%sSending async HTTP '%s' request to '%s'.",
logId, httpRequest.method(), httpRequest.url());
OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService);
SettableFuture responseFuture = SettableFuture.create();
Call requestCall = callHttpClient.newCall(buildOkHttpRequest(httpRequest, this.userAgent));
try {
requestCall.enqueue(
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
responseFuture.setException(e);
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody unused = response.body()) {
responseFuture.set(parseResponse(response));
} catch (Throwable t) {
responseFuture.setException(t);
}
}
});
} catch (Throwable t) {
responseFuture.setException(t);
}
// Makes sure cancellation state is propagated to OkHttp.
responseFuture.addListener(
() -> {
if (responseFuture.isCancelled()) {
requestCall.cancel();
}
},
directExecutor());
return responseFuture;
}
/*
* Returns a modified HTTP client that's configured to connect to the {@code networkService}'s IP
* and use its hostname in the host header, when both a hostname and an IP address is specified.
* Returns an unmodified HTTP client otherwise.
*/
private OkHttpClient clientWithHostnameAsProxy(NetworkService networkService) {
if (networkService == null) {
return this.okHttpClient;
}
String serviceIp = networkService.getNetworkEndpoint().getIpAddress().getAddress();
String serviceHostname = networkService.getNetworkEndpoint().getHostname().getName();
return this.okHttpClient
.newBuilder()
.dns(
hostname -> {
if (hostname.equals(serviceHostname)) {
hostname = serviceIp;
}
return Dns.SYSTEM.lookup(hostname);
})
.hostnameVerifier(
(hostname, session) -> {
if (trustAllCertificates) {
return true;
}
if (hostname.equals(serviceHostname)) {
return true;
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
})
.build();
}
private static Request buildOkHttpRequest(HttpRequest httpRequest, String userAgent) {
Request.Builder okRequestBuilder = new Request.Builder().url(httpRequest.url());
httpRequest.headers().names().stream()
.filter(headerName -> !Ascii.equalsIgnoreCase(headerName, USER_AGENT))
.forEach(
headerName ->
httpRequest
.headers()
.getAll(headerName)
.forEach(headerValue -> okRequestBuilder.addHeader(headerName, headerValue)));
okRequestBuilder.addHeader(USER_AGENT, userAgent);
switch (httpRequest.method()) {
case GET:
okRequestBuilder.get();
break;
case HEAD:
okRequestBuilder.head();
break;
case PUT:
okRequestBuilder.put(buildRequestBody(httpRequest));
break;
case POST:
okRequestBuilder.post(buildRequestBody(httpRequest));
break;
case DELETE:
okRequestBuilder.delete(buildRequestBody(httpRequest));
break;
}
return okRequestBuilder.build();
}
private static RequestBody buildRequestBody(HttpRequest httpRequest) {
MediaType mediaType =
MediaType.parse(
httpRequest.headers().get(com.google.common.net.HttpHeaders.CONTENT_TYPE).orElse(""));
return RequestBody.create(
mediaType, httpRequest.requestBody().orElse(ByteString.EMPTY).toByteArray());
}
private static HttpResponse parseResponse(Response okResponse) throws IOException {
logger.atInfo().log(
"Received HTTP response with code '%d' for request to '%s'.",
okResponse.code(), okResponse.request().url());
HttpResponse.Builder httpResponseBuilder =
HttpResponse.builder()
.setStatus(HttpStatus.fromCode(okResponse.code()))
.setHeaders(convertHeaders(okResponse.headers()))
.setResponseUrl(okResponse.request().url());
if (!okResponse.request().method().equals(HttpMethod.HEAD.name())
&& okResponse.body() != null) {
httpResponseBuilder.setBodyBytes(ByteString.copyFrom(okResponse.body().bytes()));
}
return httpResponseBuilder.build();
}
private static HttpHeaders convertHeaders(Headers headers) {
HttpHeaders.Builder headersBuilder = HttpHeaders.builder();
for (int i = 0; i < headers.size(); i++) {
headersBuilder.addHeader(headers.name(i), headers.value(i));
}
return headersBuilder.build();
}
/**
* Returns a {@link Builder} that allows client code to modify the configurations of the internal
* http client.
*
* @return the {@link Builder} for modifying this client instance.
*/
@Override
@SuppressWarnings("unchecked") // safe covariant cast
public Builder modify() {
return new OkHttpHttpClientBuilder(this);
}
/** Builder for {@link OkHttpHttpClient}. */
// TODO(b/145315535): add more configurable options into the builder.
public static class OkHttpHttpClientBuilder extends Builder {
private final OkHttpClient okHttpClient;
private boolean followRedirects;
private boolean trustAllCertificates;
private final ConnectionFactory connectionFactory;
private String logId;
private Duration connectionTimeout;
private String userAgent;
private OkHttpHttpClientBuilder(OkHttpHttpClient okHttpHttpClient) {
this.okHttpClient = okHttpHttpClient.okHttpClient;
this.followRedirects = okHttpClient.followRedirects();
this.trustAllCertificates = okHttpHttpClient.trustAllCertificates;
this.connectionFactory = okHttpHttpClient.connectionFactory;
this.logId = okHttpHttpClient.logId;
this.connectionTimeout = okHttpHttpClient.connectionTimeout;
this.userAgent = okHttpHttpClient.userAgent;
}
@Override
public OkHttpHttpClientBuilder setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
@Override
public OkHttpHttpClientBuilder setLogId(String logId) {
this.logId = logId;
return this;
}
@Override
public OkHttpHttpClientBuilder setConnectTimeout(Duration connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
@CanIgnoreReturnValue
public OkHttpHttpClientBuilder setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
@Override
public OkHttpHttpClient build() {
return new OkHttpHttpClient(
okHttpClient.newBuilder().followRedirects(followRedirects).build(),
trustAllCertificates,
connectionFactory,
logId,
connectionTimeout,
userAgent);
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/javanet/ConnectionFactory.java
================================================
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http.javanet;
import java.io.IOException;
import java.net.HttpURLConnection;
/** Given an URL, produces an {@link HttpURLConnection}. */
public interface ConnectionFactory {
/**
* Creates a new {@link HttpURLConnection} from the given {@code url}.
*
* @param url the URL to which the connection will be made
* @return the created connection object, which will still be in the pre-connected state
* @throws IOException if there was a problem producing the connection
*/
HttpURLConnection openConnection(String url) throws IOException;
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/http/javanet/DefaultConnectionFactory.java
================================================
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http.javanet;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
/**
* Default implementation of {@link ConnectionFactory}, which simply attempts to open the connection
* and optionally allows trusting all certifications.
*/
public class DefaultConnectionFactory implements ConnectionFactory {
private final boolean trustAllCertificates;
private final SSLSocketFactory trustAllCertsSocketFactory;
private final Duration connectTimeout;
private final Duration readTimeout;
public DefaultConnectionFactory(
boolean trustAllCertificates,
SSLSocketFactory trustAllCertsSocketFactory,
Duration connectTimeout,
Duration readTimeout) {
this.trustAllCertificates = trustAllCertificates;
this.trustAllCertsSocketFactory = checkNotNull(trustAllCertsSocketFactory);
this.connectTimeout = checkNotNull(connectTimeout);
this.readTimeout = checkNotNull(readTimeout);
}
@Override
public HttpURLConnection openConnection(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout((int) connectTimeout.toMillis());
connection.setReadTimeout((int) readTimeout.toMillis());
if (connection instanceof HttpsURLConnection && trustAllCertificates) {
HttpsURLConnection secureConnection = (HttpsURLConnection) connection;
secureConnection.setSSLSocketFactory(trustAllCertsSocketFactory);
secureConnection.setHostnameVerifier((hostname, session) -> true);
}
return connection;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/socket/DefaultTsunamiSocketFactory.java
================================================
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.flogger.GoogleLogger;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.time.Duration;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* Default implementation of {@link TsunamiSocketFactory} that creates TCP sockets.
*
*
This implementation wraps the standard Java {@link SocketFactory} and {@link SSLSocketFactory}
* to ensure that all created sockets have proper timeout settings configured, preventing plugins
* from hanging indefinitely.
*/
public final class DefaultTsunamiSocketFactory implements TsunamiSocketFactory {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final SocketFactory socketFactory;
private final SSLSocketFactory sslSocketFactory;
private final Duration defaultConnectTimeout;
private final Duration defaultReadTimeout;
/**
* Creates a new DefaultTsunamiSocketFactory.
*
* @param socketFactory the underlying socket factory to use for plain TCP connections
* @param sslSocketFactory the underlying SSL socket factory to use for SSL/TLS connections
* @param defaultConnectTimeout the default timeout for establishing connections
* @param defaultReadTimeout the default timeout for read operations
*/
public DefaultTsunamiSocketFactory(
SocketFactory socketFactory,
SSLSocketFactory sslSocketFactory,
Duration defaultConnectTimeout,
Duration defaultReadTimeout) {
this.socketFactory = checkNotNull(socketFactory);
this.sslSocketFactory = checkNotNull(sslSocketFactory);
this.defaultConnectTimeout = checkNotNull(defaultConnectTimeout);
this.defaultReadTimeout = checkNotNull(defaultReadTimeout);
checkArgument(!defaultConnectTimeout.isNegative(), "Connect timeout cannot be negative");
checkArgument(!defaultReadTimeout.isNegative(), "Read timeout cannot be negative");
logger.atInfo().log(
"TsunamiSocketFactory initialized with connect timeout: %s, read timeout: %s",
defaultConnectTimeout, defaultReadTimeout);
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return createSocket(host, port, defaultConnectTimeout, defaultReadTimeout);
}
@Override
public Socket createSocket(String host, int port, Duration timeout) throws IOException {
return createSocket(host, port, timeout, timeout);
}
@Override
public Socket createSocket(String host, int port, Duration connectTimeout, Duration readTimeout)
throws IOException {
checkNotNull(host);
checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535");
checkNotNull(connectTimeout);
checkNotNull(readTimeout);
logger.atFine().log(
"Creating socket to %s:%d with connect timeout %s, read timeout %s",
host, port, connectTimeout, readTimeout);
Socket socket = socketFactory.createSocket();
configureAndConnect(socket, new InetSocketAddress(host, port), connectTimeout, readTimeout);
return socket;
}
@Override
public Socket createSocket(InetAddress address, int port) throws IOException {
return createSocket(address, port, defaultConnectTimeout, defaultReadTimeout);
}
@Override
public Socket createSocket(InetAddress address, int port, Duration timeout) throws IOException {
return createSocket(address, port, timeout, timeout);
}
@Override
public Socket createSocket(
InetAddress address, int port, Duration connectTimeout, Duration readTimeout)
throws IOException {
checkNotNull(address);
checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535");
checkNotNull(connectTimeout);
checkNotNull(readTimeout);
logger.atFine().log(
"Creating socket to %s:%d with connect timeout %s, read timeout %s",
address.getHostAddress(), port, connectTimeout, readTimeout);
Socket socket = socketFactory.createSocket();
configureAndConnect(socket, new InetSocketAddress(address, port), connectTimeout, readTimeout);
return socket;
}
@Override
public Socket createUnconnectedSocket() throws IOException {
Socket socket = socketFactory.createSocket();
socket.setSoTimeout((int) defaultReadTimeout.toMillis());
logger.atFine().log("Created unconnected socket with read timeout %s", defaultReadTimeout);
return socket;
}
@Override
public SSLSocket createSslSocket(String host, int port) throws IOException {
return createSslSocket(host, port, defaultConnectTimeout, defaultReadTimeout);
}
@Override
public SSLSocket createSslSocket(String host, int port, Duration timeout) throws IOException {
return createSslSocket(host, port, timeout, timeout);
}
@Override
public SSLSocket createSslSocket(
String host, int port, Duration connectTimeout, Duration readTimeout) throws IOException {
checkNotNull(host);
checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535");
checkNotNull(connectTimeout);
checkNotNull(readTimeout);
logger.atFine().log(
"Creating SSL socket to %s:%d with connect timeout %s, read timeout %s",
host, port, connectTimeout, readTimeout);
// Create a plain socket first, connect with timeout, then wrap with SSL
Socket plainSocket = socketFactory.createSocket();
configureAndConnect(
plainSocket, new InetSocketAddress(host, port), connectTimeout, readTimeout);
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(plainSocket, host, port, true);
sslSocket.setSoTimeout((int) readTimeout.toMillis());
sslSocket.startHandshake();
return sslSocket;
}
@Override
public SSLSocket createSslSocket(InetAddress address, int port) throws IOException {
return createSslSocket(address, port, defaultConnectTimeout, defaultReadTimeout);
}
@Override
public SSLSocket createSslSocket(InetAddress address, int port, Duration timeout)
throws IOException {
return createSslSocket(address, port, timeout, timeout);
}
@Override
public SSLSocket createSslSocket(
InetAddress address, int port, Duration connectTimeout, Duration readTimeout)
throws IOException {
checkNotNull(address);
checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535");
checkNotNull(connectTimeout);
checkNotNull(readTimeout);
logger.atFine().log(
"Creating SSL socket to %s:%d with connect timeout %s, read timeout %s",
address.getHostAddress(), port, connectTimeout, readTimeout);
// Create a plain socket first, connect with timeout, then wrap with SSL
Socket plainSocket = socketFactory.createSocket();
configureAndConnect(
plainSocket, new InetSocketAddress(address, port), connectTimeout, readTimeout);
SSLSocket sslSocket =
(SSLSocket)
sslSocketFactory.createSocket(plainSocket, address.getHostAddress(), port, true);
sslSocket.setSoTimeout((int) readTimeout.toMillis());
sslSocket.startHandshake();
return sslSocket;
}
@Override
public SSLSocket wrapWithSsl(Socket socket, String host, int port, boolean autoClose)
throws IOException {
checkNotNull(socket);
checkNotNull(host);
checkArgument(port > 0 && port <= 65535, "Port must be between 1 and 65535");
logger.atFine().log("Wrapping existing socket with SSL for host %s:%d", host, port);
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, autoClose);
// Preserve the timeout from the original socket if set, otherwise use default
int originalTimeout = socket.getSoTimeout();
if (originalTimeout > 0) {
sslSocket.setSoTimeout(originalTimeout);
} else {
sslSocket.setSoTimeout((int) defaultReadTimeout.toMillis());
}
sslSocket.startHandshake();
return sslSocket;
}
@Override
public Duration getDefaultConnectTimeout() {
return defaultConnectTimeout;
}
@Override
public Duration getDefaultReadTimeout() {
return defaultReadTimeout;
}
/**
* Configures socket options and connects to the specified address with timeout.
*
* @param socket the socket to configure and connect
* @param address the address to connect to
* @param connectTimeout the timeout for establishing the connection
* @param readTimeout the timeout for read operations
* @throws IOException if an I/O error occurs
*/
private void configureAndConnect(
Socket socket, InetSocketAddress address, Duration connectTimeout, Duration readTimeout)
throws IOException {
// Set read timeout before connecting
socket.setSoTimeout((int) readTimeout.toMillis());
// Enable TCP keep-alive to detect dead connections
socket.setKeepAlive(true);
// Disable Nagle's algorithm for better latency in security scanning
socket.setTcpNoDelay(true);
// Connect with timeout
socket.connect(address, (int) connectTimeout.toMillis());
logger.atFine().log(
"Socket connected to %s with SO_TIMEOUT=%dms", address, readTimeout.toMillis());
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactory.java
================================================
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.time.Duration;
import javax.net.ssl.SSLSocket;
/**
* A socket factory API for creating TCP sockets with enforced default configurations.
*
*
This API provides a normalized way to create sockets from Tsunami plugins, ensuring that
* proper timeouts are always configured. This prevents plugins from hanging indefinitely when
* servers don't respond.
*
*
Plugins should use this factory instead of directly creating sockets through {@link
* javax.net.SocketFactory} or {@link javax.net.ssl.SSLSocketFactory} to ensure consistent behavior
* and proper timeout handling.
*
*
*/
public interface TsunamiSocketFactory {
/**
* Creates a TCP socket connected to the specified host and port with default timeouts.
*
*
The socket will be configured with the default connect and read timeouts specified in the
* Tsunami configuration. If no configuration is provided, sensible defaults will be used.
*
* @param host the host to connect to
* @param port the port to connect to
* @return a connected socket with enforced timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(String host, int port) throws IOException;
/**
* Creates a TCP socket connected to the specified host and port with a single timeout.
*
*
The specified timeout will be used for both connection establishment and read operations.
*
* @param host the host to connect to
* @param port the port to connect to
* @param timeout the timeout for both connection and read operations
* @return a connected socket with the specified timeout
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(String host, int port, Duration timeout) throws IOException;
/**
* Creates a TCP socket connected to the specified host and port with custom timeouts.
*
* @param host the host to connect to
* @param port the port to connect to
* @param connectTimeout the timeout for establishing the connection
* @param readTimeout the timeout for read operations (SO_TIMEOUT)
* @return a connected socket with the specified timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(String host, int port, Duration connectTimeout, Duration readTimeout)
throws IOException;
/**
* Creates a TCP socket connected to the specified address and port with default timeouts.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @return a connected socket with enforced timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(InetAddress address, int port) throws IOException;
/**
* Creates a TCP socket connected to the specified address and port with a single timeout.
*
*
The specified timeout will be used for both connection establishment and read operations.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @param timeout the timeout for both connection and read operations
* @return a connected socket with the specified timeout
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(InetAddress address, int port, Duration timeout) throws IOException;
/**
* Creates a TCP socket connected to the specified address and port with custom timeouts.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @param connectTimeout the timeout for establishing the connection
* @param readTimeout the timeout for read operations (SO_TIMEOUT)
* @return a connected socket with the specified timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createSocket(InetAddress address, int port, Duration connectTimeout, Duration readTimeout)
throws IOException;
/**
* Creates an unconnected TCP socket with default timeouts configured.
*
*
The returned socket will have SO_TIMEOUT set to the default read timeout. The caller is
* responsible for connecting the socket, which should be done with a timeout.
*
* @return an unconnected socket with default read timeout configured
* @throws IOException if an I/O error occurs while creating the socket
*/
Socket createUnconnectedSocket() throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified host and port with default timeouts.
*
*
The socket will be configured with the default connect and read timeouts. SSL/TLS handshake
* will be performed automatically.
*
* @param host the host to connect to
* @param port the port to connect to
* @return a connected SSL socket with enforced timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(String host, int port) throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified host and port with a single timeout.
*
*
The specified timeout will be used for both connection establishment and read operations.
*
* @param host the host to connect to
* @param port the port to connect to
* @param timeout the timeout for both connection and read operations
* @return a connected SSL socket with the specified timeout
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(String host, int port, Duration timeout) throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified host and port with custom timeouts.
*
* @param host the host to connect to
* @param port the port to connect to
* @param connectTimeout the timeout for establishing the connection
* @param readTimeout the timeout for read operations (SO_TIMEOUT)
* @return a connected SSL socket with the specified timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(String host, int port, Duration connectTimeout, Duration readTimeout)
throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified address and port with default timeouts.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @return a connected SSL socket with enforced timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(InetAddress address, int port) throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified address and port with a single timeout.
*
*
The specified timeout will be used for both connection establishment and read operations.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @param timeout the timeout for both connection and read operations
* @return a connected SSL socket with the specified timeout
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(InetAddress address, int port, Duration timeout) throws IOException;
/**
* Creates an SSL/TLS socket connected to the specified address and port with custom timeouts.
*
* @param address the IP address to connect to
* @param port the port to connect to
* @param connectTimeout the timeout for establishing the connection
* @param readTimeout the timeout for read operations (SO_TIMEOUT)
* @return a connected SSL socket with the specified timeouts
* @throws IOException if an I/O error occurs while creating the socket
*/
SSLSocket createSslSocket(
InetAddress address, int port, Duration connectTimeout, Duration readTimeout)
throws IOException;
/**
* Wraps an existing socket with SSL/TLS.
*
*
This method is useful when you need to upgrade a plain TCP connection to SSL/TLS, such as
* when implementing STARTTLS protocols.
*
* @param socket the existing socket to wrap
* @param host the hostname for SNI (Server Name Indication)
* @param port the port number
* @param autoClose whether the underlying socket should be closed when the SSL socket is closed
* @return an SSL socket wrapping the existing socket
* @throws IOException if an I/O error occurs while creating the SSL socket
*/
SSLSocket wrapWithSsl(Socket socket, String host, int port, boolean autoClose) throws IOException;
/**
* Returns the default connect timeout configured for this factory.
*
* @return the default connect timeout
*/
Duration getDefaultConnectTimeout();
/**
* Returns the default read timeout configured for this factory.
*
* @return the default read timeout
*/
Duration getDefaultReadTimeout();
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryCliOptions.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.tsunami.common.cli.CliOption;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Command line arguments for {@link TsunamiSocketFactory}.
*
*
These options allow users to override socket timeout settings from the command line. CLI
* options take precedence over configuration file settings.
*/
@Parameters(separators = "=")
public final class TsunamiSocketFactoryCliOptions implements CliOption {
@Parameter(
names = "--socket-connect-timeout-seconds",
description =
"The timeout in seconds for establishing TCP connections. This timeout applies to the"
+ " socket connect operation. Default is 10 seconds.")
public Integer connectTimeoutSeconds;
@Parameter(
names = "--socket-read-timeout-seconds",
description =
"The timeout in seconds for read operations on sockets (SO_TIMEOUT). If no data is"
+ " received within this time, a SocketTimeoutException will be thrown. Default is 30"
+ " seconds.")
public Integer readTimeoutSeconds;
@Parameter(
names = "--socket-trust-all-certificates",
arity = 1,
description =
"Whether SSL/TLS connections should trust all certificates. When true, accepts any SSL"
+ " certificate without validation. Useful for scanning targets with self-signed"
+ " certificates. Default is true.")
public Boolean trustAllCertificates;
@Parameter(
names = "--socket-disable-timeouts",
arity = 1,
description =
"Disable all socket timeouts, allowing connections to wait indefinitely. WARNING: This"
+ " can cause plugins to hang forever if servers do not respond. Use with caution."
+ " Default is false.")
public Boolean disableTimeouts;
@Override
public void validate() {
// Skip timeout validation if timeouts are disabled
if (disableTimeouts != null && disableTimeouts) {
return;
}
validateTimeout("--socket-connect-timeout-seconds", connectTimeoutSeconds);
validateTimeout("--socket-read-timeout-seconds", readTimeoutSeconds);
}
private static void validateTimeout(String flagName, @Nullable Integer value) {
if (value != null && value < 0) {
throw new ParameterException(
String.format("%s cannot be a negative number, received %d.", flagName, value));
}
if (value != null && value == 0) {
throw new ParameterException(
String.format(
"%s cannot be zero. Use a positive value for timeouts or pass the"
+ " --socket-disable-timeouts flag to disable timeouts entirely. Received %d.",
flagName, value));
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryConfigProperties.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import com.google.tsunami.common.config.annotations.ConfigProperties;
/**
* Configuration properties for {@link TsunamiSocketFactory}.
*
*
These properties can be set in the Tsunami configuration file (e.g., tsunami.yaml) under the
* {@code common.net.socket} prefix:
*
*
*/
@ConfigProperties("common.net.socket")
public final class TsunamiSocketFactoryConfigProperties {
/**
* The timeout in seconds for establishing TCP connections.
*
*
This timeout applies to the socket connect operation. If the connection cannot be
* established within this time, a {@link java.net.SocketTimeoutException} will be thrown.
*
*
Default value is 10 seconds if not specified.
*/
Integer connectTimeoutSeconds;
/**
* The timeout in seconds for read operations on sockets.
*
*
This timeout is set as the socket's SO_TIMEOUT option. If no data is received within this
* time, a {@link java.net.SocketTimeoutException} will be thrown.
*
*
Default value is 30 seconds if not specified. This is intentionally longer than the connect
* timeout to allow for slow servers or large data transfers.
*/
Integer readTimeoutSeconds;
/**
* Whether SSL/TLS connections should trust all certificates.
*
*
When set to true, the socket factory will accept any SSL certificate without validation.
* This is useful for security scanning where targets may have self-signed or expired
* certificates.
*
*
Warning: This should only be used in controlled environments. Setting this
* to true disables certificate validation which could expose the scanner to man-in-the-middle
* attacks.
*
*
Default value is true for security scanning purposes.
*/
Boolean trustAllCertificates;
/**
* Whether to disable all socket timeouts.
*
*
When set to true, sockets will wait indefinitely for connections and data. This means
* plugins may hang forever if a server does not respond.
*
*
Warning: Disabling timeouts is dangerous and can cause the scanner to hang
* indefinitely. Only use this option if you have a specific need to wait for slow or unresponsive
* servers.
*
*
Default value is false (timeouts are enforced).
*/
Boolean disableTimeouts;
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryModule.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Guice module for installing {@link TsunamiSocketFactory}.
*
*
This module provides a {@link TsunamiSocketFactory} instance that creates sockets with
* enforced timeout configurations. It integrates with Tsunami's configuration system to allow
* customization of default timeouts through both configuration files and command-line options.
*
*
Example usage in a plugin:
*
*
{@code
* public class MyPlugin implements VulnDetector {
* private final TsunamiSocketFactory socketFactory;
*
* @Inject
* MyPlugin(TsunamiSocketFactory socketFactory) {
* this.socketFactory = socketFactory;
* }
*
* public void doSomething() throws IOException {
* // Socket will have enforced timeouts
* Socket socket = socketFactory.createSocket("example.com", 80);
* // ...
* }
* }
* }
*/
public final class TsunamiSocketFactoryModule extends AbstractModule {
// Default timeout values
private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
private static final int DEFAULT_READ_TIMEOUT_SECONDS = 30;
private static final boolean DEFAULT_TRUST_ALL_CERTIFICATES = true;
private static final boolean DEFAULT_DISABLE_TIMEOUTS = false;
// This TrustManager does NOT validate certificate chains.
private static final X509TrustManager TRUST_ALL_CERTS_MANAGER =
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
@Override
protected void configure() {
// Module configuration is handled by @Provides methods
}
@Provides
@Singleton
TsunamiSocketFactory provideTsunamiSocketFactory(
@TrustAllCertificates boolean trustAllCertificates,
@DisableTimeouts boolean disableTimeouts,
@ConnectTimeoutSeconds int connectTimeoutSeconds,
@ReadTimeoutSeconds int readTimeoutSeconds)
throws GeneralSecurityException {
SocketFactory socketFactory = SocketFactory.getDefault();
SSLSocketFactory sslSocketFactory;
if (trustAllCertificates) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {TRUST_ALL_CERTS_MANAGER}, new SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();
} else {
sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
}
// When timeouts are disabled, use 0 which means infinite timeout in Java
Duration connectTimeout =
disableTimeouts ? Duration.ZERO : Duration.ofSeconds(connectTimeoutSeconds);
Duration readTimeout = disableTimeouts ? Duration.ZERO : Duration.ofSeconds(readTimeoutSeconds);
return new DefaultTsunamiSocketFactory(
socketFactory, sslSocketFactory, connectTimeout, readTimeout);
}
@Provides
@DisableTimeouts
boolean provideDisableTimeouts(
TsunamiSocketFactoryCliOptions cliOptions,
TsunamiSocketFactoryConfigProperties configProperties) {
if (cliOptions.disableTimeouts != null) {
return cliOptions.disableTimeouts;
}
if (configProperties.disableTimeouts != null) {
return configProperties.disableTimeouts;
}
return DEFAULT_DISABLE_TIMEOUTS;
}
@Provides
@TrustAllCertificates
boolean provideTrustAllCertificates(
TsunamiSocketFactoryCliOptions cliOptions,
TsunamiSocketFactoryConfigProperties configProperties) {
if (cliOptions.trustAllCertificates != null) {
return cliOptions.trustAllCertificates;
}
if (configProperties.trustAllCertificates != null) {
return configProperties.trustAllCertificates;
}
return DEFAULT_TRUST_ALL_CERTIFICATES;
}
@Provides
@ConnectTimeoutSeconds
int provideConnectTimeoutSeconds(
TsunamiSocketFactoryCliOptions cliOptions,
TsunamiSocketFactoryConfigProperties configProperties) {
if (cliOptions.connectTimeoutSeconds != null) {
return cliOptions.connectTimeoutSeconds;
}
if (configProperties.connectTimeoutSeconds != null) {
return configProperties.connectTimeoutSeconds;
}
return DEFAULT_CONNECT_TIMEOUT_SECONDS;
}
@Provides
@ReadTimeoutSeconds
int provideReadTimeoutSeconds(
TsunamiSocketFactoryCliOptions cliOptions,
TsunamiSocketFactoryConfigProperties configProperties) {
if (cliOptions.readTimeoutSeconds != null) {
return cliOptions.readTimeoutSeconds;
}
if (configProperties.readTimeoutSeconds != null) {
return configProperties.readTimeoutSeconds;
}
return DEFAULT_READ_TIMEOUT_SECONDS;
}
/** Qualifier for whether to disable all socket timeouts. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface DisableTimeouts {}
/** Qualifier for whether to trust all SSL certificates. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface TrustAllCertificates {}
/** Qualifier for the connect timeout in seconds. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface ConnectTimeoutSeconds {}
/** Qualifier for the read timeout in seconds. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface ReadTimeoutSeconds {}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/reflection/ClassGraphModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.reflection;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.AbstractModule;
import io.github.classgraph.ScanResult;
/** Guice module for providing ClassGraph bindings. */
public final class ClassGraphModule extends AbstractModule {
private final ScanResult scanResult;
public ClassGraphModule(ScanResult scanResult) {
this.scanResult = checkNotNull(scanResult);
}
@Override
protected void configure() {
bind(ScanResult.class).annotatedWith(RuntimeClassGraphScanResult.class).toInstance(scanResult);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/reflection/RuntimeClassGraphScanResult.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.reflection;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
/** Annotation for the ClassGraph {@link io.github.classgraph.ScanResult} at runtime. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({PARAMETER, METHOD, FIELD})
public @interface RuntimeClassGraphScanResult {}
================================================
FILE: common/src/main/java/com/google/tsunami/common/server/CompactRunRequestHelper.java
================================================
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.server;
import com.google.common.collect.ImmutableList;
import com.google.tsunami.proto.MatchedPlugin;
import com.google.tsunami.proto.NetworkService;
import com.google.tsunami.proto.RunCompactRequest;
import com.google.tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget;
import com.google.tsunami.proto.RunRequest;
import java.util.HashMap;
/**
* CompactRunRequestHelper is a helper class to compress/uncompress the RunRequest into/from the
* compact representation.
*/
public final class CompactRunRequestHelper {
private CompactRunRequestHelper() {}
public static RunCompactRequest compress(RunRequest runRequest) {
var builder = RunCompactRequest.newBuilder().setTarget(runRequest.getTarget());
HashMap serviceIndexMap = new HashMap<>();
int pluginIndex = -1;
for (MatchedPlugin matchedPlugin : runRequest.getPluginsList()) {
pluginIndex++;
builder.addPlugins(matchedPlugin.getPlugin());
for (NetworkService service : matchedPlugin.getServicesList()) {
Integer serviceIndex = serviceIndexMap.get(service);
if (serviceIndex == null) {
serviceIndex = serviceIndexMap.size();
serviceIndexMap.put(service, serviceIndex);
builder.addServices(service);
}
builder.addScanTargets(
PluginNetworkServiceTarget.newBuilder()
.setPluginIndex(pluginIndex)
.setServiceIndex(serviceIndex)
.build());
}
}
return builder.build();
}
public static RunRequest uncompress(RunCompactRequest runCompactRequest) {
ImmutableList.Builder matchedPlugins = ImmutableList.builder();
for (var target : runCompactRequest.getScanTargetsList()) {
var plugin = runCompactRequest.getPlugins(target.getPluginIndex());
var networkService = runCompactRequest.getServices(target.getServiceIndex());
matchedPlugins.add(
MatchedPlugin.newBuilder().setPlugin(plugin).addServices(networkService).build());
}
return RunRequest.newBuilder()
.setTarget(runCompactRequest.getTarget())
.addAllPlugins(matchedPlugins.build())
.build();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/server/LanguageServerCommand.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.server;
import com.google.auto.value.AutoValue;
import java.time.Duration;
import javax.annotation.Nullable;
/** Command to spawn a language server and associated command lines. */
@AutoValue
public abstract class LanguageServerCommand {
public static LanguageServerCommand create(
String serverCommand,
@Nullable String serverAddress,
String port,
String logId,
String outputDir,
boolean trustAllSsl,
Duration timeoutSeconds,
String callbackAddress,
int callbackPort,
String pollingUri,
int deadlineRunSeconds) {
return new AutoValue_LanguageServerCommand(
serverCommand,
serverAddress,
port,
logId,
outputDir,
trustAllSsl,
timeoutSeconds,
callbackAddress,
callbackPort,
pollingUri,
deadlineRunSeconds);
}
public abstract String serverCommand();
public abstract String serverAddress();
public abstract String port();
public abstract String logId();
public abstract String outputDir();
public abstract boolean trustAllSslCert();
public abstract Duration timeoutSeconds();
public abstract String callbackAddress();
public abstract int callbackPort();
public abstract String pollingUri();
public abstract int deadlineRunSeconds();
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/time/SystemUtcClockModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time;
import com.google.inject.AbstractModule;
import java.time.Clock;
/** Binds {@link java.time.Clock} to a {@code Clock.systemUTC()}. */
public class SystemUtcClockModule extends AbstractModule {
@Override
protected void configure() {
bind(Clock.class).annotatedWith(UtcClock.class).toInstance(Clock.systemUTC());
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/time/UtcClock.java
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
/** Annotation for the UTC {@link java.time.Clock} used in Tsunami. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface UtcClock {}
================================================
FILE: common/src/main/java/com/google/tsunami/common/time/testing/FakeUtcClock.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time.testing;
import static com.google.common.base.Preconditions.checkNotNull;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.concurrent.atomic.AtomicReference;
/**
* Implementation of a {@link java.time.Clock} that returns a settable {@link Instant} value.
*
*
By default, the clock is set to the {@link Instant} when the clock is created. Clock can be
* set to a specific instant by {@link #setNow}.
*/
public final class FakeUtcClock extends Clock {
private final AtomicReference nowReference = new AtomicReference<>();
private FakeUtcClock(Instant now) {
nowReference.set(checkNotNull(now));
}
/**
* Create a {@link FakeUtcClock} instance initialized to UTC now.
*
*
To create a fake UTC clock at a specific instant, calling {@code setNow()} as in:
*
*
*
* @return a {@link FakeUtcClock} instance.
*/
public static FakeUtcClock create() {
return new FakeUtcClock(Instant.now());
}
/**
* Sets the return value of {@link #instant()}.
*
* @param now the instant that this clock points to
* @return this
*/
public FakeUtcClock setNow(Instant now) {
nowReference.set(checkNotNull(now));
return this;
}
/**
* Advances the clock by the given duration.
*
*
NOTE: this method can be called with a negative duration if the clock needs to go back in
* time.
*
* @param increment the duration to advance the clock by
* @return this
*/
public FakeUtcClock advance(Duration increment) {
checkNotNull(increment);
nowReference.getAndUpdate(now -> now.plus(increment));
return this;
}
@Override
public Instant instant() {
return nowReference.get();
}
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}
@Override
public Clock withZone(ZoneId zone) {
throw new UnsupportedOperationException("Setting ZoneId to FakeUtcClock is not supported");
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FakeUtcClock) {
FakeUtcClock other = (FakeUtcClock) obj;
return nowReference.get().equals(other.nowReference.get());
}
return false;
}
@Override
public int hashCode() {
return nowReference.get().hashCode();
}
@Override
public String toString() {
return String.format("FakeUtcClock(now = %s)", nowReference.get());
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/time/testing/FakeUtcClockModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time.testing;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.AbstractModule;
import com.google.tsunami.common.time.UtcClock;
import java.time.Clock;
/** Binds {@link java.time.Clock} to a {@link FakeUtcClock}. */
public class FakeUtcClockModule extends AbstractModule {
private final FakeUtcClock fakeUtcClock;
public FakeUtcClockModule() {
this.fakeUtcClock = FakeUtcClock.create();
}
public FakeUtcClockModule(FakeUtcClock fakeUtcClock) {
this.fakeUtcClock = checkNotNull(fakeUtcClock);
}
@Override
protected void configure() {
bind(Clock.class).annotatedWith(UtcClock.class).toInstance(fakeUtcClock);
bind(FakeUtcClock.class).annotatedWith(UtcClock.class).toInstance(fakeUtcClock);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/ComparisonUtility.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import java.util.List;
/** Utility for version related comparison. */
final class ComparisonUtility {
private ComparisonUtility() {}
/**
* Compares two lists. If one list is shorter than the other, {@code fillValue} is used for
* comparison.
*
*
For example, comparing {@code [1, 2, 3]} and {@code [1, 2, 3, 4]} with {@code fillValue}
* equals to 10 is equivalent of comparing {@code [1, 2, 3, 10]} with {@code [1, 2, 3, 4]}.
*
* @param left list for comparison.
* @param right list for comparison.
* @param fillValue value to use if one list is shorter than the other.
* @param element type of the list.
* @return 0 if two lists equals, -1 if {@code left} is less than {@code right}, 1 otherwise.
*/
static > int compareListWithFillValue(
List left, List right, T fillValue) {
int longest = Math.max(left.size(), right.size());
for (int i = 0; i < longest; i++) {
T leftElement = fillValue;
T rightElement = fillValue;
if (i < left.size()) {
leftElement = left.get(i);
}
if (i < right.size()) {
rightElement = right.get(i);
}
int compareResult = leftElement.compareTo(rightElement);
if (compareResult != 0) {
return compareResult;
}
}
return 0;
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/KnownQualifier.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Ascii;
import java.util.Arrays;
/**
* A list of all the known text token qualifiers from our vulnerability feeds and the order here
* represents the precedence of these identifiers.
*/
enum KnownQualifier implements Comparable {
ALPHA("alpha"),
BETA("beta"),
PRE("pre"),
R("r"),
RC("rc"),
ABSENT(""),
P("p"),
PATCH("patch"),
PATCHED("patched");
private final String qualifierText;
KnownQualifier(String qualifierText) {
this.qualifierText = qualifierText;
}
String getQualifierText() {
return this.qualifierText;
}
static boolean isKnownQualifier(String string) {
checkNotNull(string);
return Arrays.stream(KnownQualifier.values())
.anyMatch(knownQualifier -> Ascii.equalsIgnoreCase(knownQualifier.qualifierText, string));
}
static KnownQualifier fromText(String string) {
checkNotNull(string);
return Arrays.stream(KnownQualifier.values())
.filter(knownQualifier -> Ascii.equalsIgnoreCase(knownQualifier.qualifierText, string))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("%s is not a valid KnownQualifier text.", string)));
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/Segment.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.Immutable;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The segment of a version number, separated by the semantic separators from the original version
* string.
*
*
For example, there are 2 segments in version string "2.1.1-alpha.1": "2.1.1" and "alpha.1".
*
*
The first element of a Segment should always be a {@link KnownQualifier} token. If no {@link
* KnownQualifier} is found in the segment, an ABSENT qualifier is added.
*/
@AutoValue
@Immutable
abstract class Segment implements Comparable {
static final Segment NULL = Segment.fromTokenList(ImmutableList.of(Token.EMPTY));
private static final ImmutableSet TOKENIZER_DELIMITERS =
ImmutableSet.of("\\.", "\\+", "-", ":", "_", "~");
private static final Pattern TOKENIZER_SPLIT_REGEX =
Pattern.compile(
// Additional split on boundaries between number and text.
"(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)|"
// We keep the delimiter for comparison.
+ TOKENIZER_DELIMITERS.stream()
.map(delimiter -> String.format("((?<=%1$s)|(?=%1$s))", delimiter))
.collect(Collectors.joining("|")));
private static final ImmutableSet EXCLUDED_TOKENS = ImmutableSet.of(".", "gg", "N/A");
/** All the tokens within this segment. */
abstract ImmutableList tokens();
static Segment fromTokenList(ImmutableList tokens) {
ImmutableList.Builder finalTokensBuilder = new ImmutableList.Builder<>();
// Make sure Segment always starts with a KnownQualifier. See http://b/135912609 for details.
if (tokens.isEmpty() || !tokens.get(0).isKnownQualifier()) {
finalTokensBuilder.add(Token.fromKnownQualifier(KnownQualifier.ABSENT));
}
finalTokensBuilder.addAll(tokens);
return new AutoValue_Segment(finalTokensBuilder.build());
}
static Segment fromString(String segmentString) {
return parseFromString(segmentString);
}
private static Segment parseFromString(String segmentString) {
ImmutableList rawTokens =
Arrays.stream(TOKENIZER_SPLIT_REGEX.split(segmentString))
.filter(token -> !token.isEmpty())
.filter(token -> !EXCLUDED_TOKENS.contains(token))
.collect(ImmutableList.toImmutableList());
if (rawTokens.isEmpty()) {
return Segment.NULL;
}
ImmutableList.Builder tokensBuilder = new ImmutableList.Builder<>();
// Make sure Segment always starts with a KnownQualifier. See http://b/135912609 for details.
if (!KnownQualifier.isKnownQualifier(rawTokens.get(0))) {
tokensBuilder.add(Token.fromKnownQualifier(KnownQualifier.ABSENT));
}
// Parses token.
for (String rawToken : rawTokens) {
try {
long numericToken = Long.parseLong(rawToken);
tokensBuilder.add(Token.fromNumeric(numericToken));
} catch (NumberFormatException e) {
tokensBuilder.add(Token.fromText(rawToken));
}
}
return Segment.fromTokenList(tokensBuilder.build());
}
@Override
public int compareTo(Segment other) {
return ComparisonUtility.compareListWithFillValue(this.tokens(), other.tokens(), Token.EMPTY);
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/Token.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import com.google.auto.value.AutoOneOf;
import com.google.common.base.Ascii;
import com.google.errorprone.annotations.Immutable;
/**
* Represents a token from a version string.
*
*
A token is the smallest meaningful piece of a version string. For example, there are 3 tokens
* in version 2.1.1: [token(2), token(1), token(1)].
*/
@Immutable
@AutoOneOf(Token.Kind.class)
abstract class Token implements Comparable {
static final Token EMPTY = Token.fromKnownQualifier(KnownQualifier.ABSENT);
/** All types of a token, required by the AutoOneOf annotation. */
public enum Kind {
NUMERIC,
TEXT
}
abstract Kind getKind();
abstract long getNumeric();
static Token fromNumeric(long numeric) {
return AutoOneOf_Token.numeric(numeric);
}
boolean isNumeric() {
return getKind().equals(Kind.NUMERIC);
}
abstract String getText();
static Token fromText(String string) {
return AutoOneOf_Token.text(Ascii.toLowerCase(string));
}
boolean isText() {
return getKind().equals(Kind.TEXT);
}
static Token fromKnownQualifier(KnownQualifier knownQualifier) {
return AutoOneOf_Token.text(knownQualifier.getQualifierText());
}
boolean isKnownQualifier() {
return isText() && KnownQualifier.isKnownQualifier(getText());
}
boolean isEmptyToken() {
return isText() && getText().isEmpty();
}
@Override
public int compareTo(Token other) {
// Empty tokens are always the same.
if (this.isEmptyToken() && other.isEmptyToken()) {
return 0;
}
/*
* If the tokens under comparison are one Empty token and one Numeric token, then Empty token
* should always be less than Numeric token, e.g. version 2.1 is less than 2.1.1 (i.e.
* 2.1.empty < 2.1.1, empty < 1).
*/
if (this.isEmptyToken() && other.isNumeric()) {
return -1;
}
if (this.isNumeric() && other.isEmptyToken()) {
return 1;
}
/*
* For Numeric and Text tokens, we follow the specification from http://semver.org#spec-item-11:
* 1. Numeric tokens are compared numerically.
* 2. Text tokens are compared lexically, case insensitively.
* 3. Numeric identifiers always have lower precedence than non-numeric identifiers.
*
* For all the known qualifiers, we apply the comparison rules defined by KnownQualifier.
*/
if (this.isNumeric() && other.isNumeric()) {
return Long.compare(this.getNumeric(), other.getNumeric());
}
if (this.isKnownQualifier() && other.isKnownQualifier()) {
return KnownQualifier.fromText(this.getText())
.compareTo(KnownQualifier.fromText(other.getText()));
}
if (this.isText() && other.isText()) {
return this.getText().compareToIgnoreCase(other.getText());
}
// Cross type comparison, Numeric token is always less than Text tokens.
return this.getKind().compareTo(other.getKind());
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/Version.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Software version class that support 3 types. Type {@link Type#NORMAL} with a version number, type
* {@link Type#MAXIMUM} and {@link Type#MINIMUM} for use in version range to indicate unbounded
* ranges.
*
*
Version is suitable for unstructured version string and supports comparison operations using
* extra logic that detects semantic qualifier.
*
*
The current logic support comparison of versions that respect order, like:
*
*
*
* Known limitation of this approach are versions with no order, like commit hashes.
*/
@AutoValue
@Immutable
public abstract class Version implements Comparable {
private static final Pattern EPOCH_PATTERN = Pattern.compile("\\d+[:|_].*");
private static final Pattern SEMANTIC_SEGMENT_SEPARATORS = Pattern.compile("[-:_~]");
private static final Version MAXIMUM =
builder().setVersionType(Type.MAXIMUM).setVersionString("").build();
private static final Version MINIMUM =
builder().setVersionType(Type.MINIMUM).setVersionString("").build();
/**
* Software version class that support 3 types. Type {@link Type#NORMAL} with a version number,
* type {@link Type#MAXIMUM} and {@link Type#MINIMUM} for use in version range to indicate
* unbounded ranges.
*/
public enum Type {
NORMAL,
MINIMUM,
MAXIMUM
}
abstract Type versionType();
public abstract String versionString();
@Memoized
ImmutableList segments() {
String normalizedString = versionString();
// Add a default epoch of 0 if one is missing.
if (!EPOCH_PATTERN.matcher(normalizedString).matches()) {
normalizedString = "0:" + normalizedString;
}
return Arrays.stream(SEMANTIC_SEGMENT_SEPARATORS.split(normalizedString))
.filter(segment -> !segment.isEmpty())
.map(Segment::fromString)
.filter(segment -> !segment.equals(Segment.NULL))
.collect(ImmutableList.toImmutableList());
}
public static Builder builder() {
return new AutoValue_Version.Builder();
}
public static Version fromString(String versionString) {
checkArgument(!Strings.isNullOrEmpty(versionString));
Version version = builder().setVersionType(Type.NORMAL).setVersionString(versionString).build();
if (!EPOCH_PATTERN.matcher(versionString).matches()) {
versionString = "0:" + versionString;
}
boolean isValid =
version.segments().stream()
.flatMap(segment -> segment.tokens().stream())
.anyMatch(
token ->
(token.isNumeric() && token.getNumeric() != 0)
|| (token.isText() && !token.getText().isEmpty()));
if (!isValid) {
throw new IllegalArgumentException(
String.format(
"Input version string %s is not valid, it should contain at least one non-empty"
+ " field.",
versionString));
}
return version;
}
public static Version maximum() {
return MAXIMUM;
}
public boolean isMaximum() {
return versionType().equals(Type.MAXIMUM);
}
public static Version minimum() {
return MINIMUM;
}
public boolean isMinimum() {
return versionType().equals(Type.MINIMUM);
}
/**
* Compare this Version object with the other one using their meaningful segments.
*
*
IMPORTANT: This compareTo implementation is NOT consistent with {@link
* Version#equals(Object)} method, i.e. this.compareTo(that) == 0 does not imply
* this.equals(that). The reason is that the raw version string is tokenized and certain tokens
* are ignored. Tokenized strings are used for {@link #compareTo} comparison while raw version
* string is used for {@link #equals} comparison. Be careful when using {@link Version} object in
* collections like {@code HashMap} or {@code TreeMap}.
*
* @param other the other {@link Version} object to be compared with.
* @return 0 if the segments of the two {@link Version} objects are the same, -1 if this {@link
* Version} is less than {@code other}, 1 if this {@link Version} is greater than {@code
* other}.
*/
@Override
public int compareTo(Version other) {
if ((this.isMinimum() && other.isMinimum()) || (this.isMaximum() && other.isMaximum())) {
return 0;
}
if (this.isMinimum() || other.isMaximum()) {
return -1;
}
if (this.isMaximum() || other.isMinimum()) {
return 1;
}
return ComparisonUtility.compareListWithFillValue(
this.segments(), other.segments(), Segment.NULL);
}
public boolean isLessThan(Version version) {
return this.compareTo(version) < 0;
}
/** Builder for {@link Version}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setVersionType(Type value);
public abstract Builder setVersionString(String value);
public abstract Version build();
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/VersionRange.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.Immutable;
/** Immutable version range, e.g. [1.0, 2.0), (,3.0), etc. */
@AutoValue
@Immutable
public abstract class VersionRange {
/** The inclusiveness of the range endpoint. */
public enum Inclusiveness {
INCLUSIVE,
EXCLUSIVE
}
public abstract Version minVersion();
public abstract Inclusiveness minVersionInclusiveness();
public abstract Version maxVersion();
public abstract Inclusiveness maxVersionInclusiveness();
public static Builder builder() {
return new AutoValue_VersionRange.Builder();
}
/** Builder for {@link VersionRange}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setMinVersion(Version value);
public abstract Builder setMinVersionInclusiveness(Inclusiveness value);
public abstract Builder setMaxVersion(Version value);
public abstract Builder setMaxVersionInclusiveness(Inclusiveness value);
public abstract VersionRange build();
}
/**
* Parses the given {@code rangeString} and generates a {@link VersionRange} object.
*
*
Valid strings for a version range are like:
*
*
*
(,1.0]: from negative infinity to version 1.0 (inclusive).
*
(,1.0): from negative infinity to version 1.0 (exclusive).
*
[1.0,): from version 1.0 (inclusive) to positive infinity.
*
(1.0,): from version 1.0 (exclusive) to positive infinity.
*
[1.0,2.0): from version 1.0 (inclusive) to version 2.0 (exclusive).
*
*
* @param rangeString the string representation of a version range.
* @return the parsed {@link VersionRange} object from the given string.
*/
public static VersionRange parse(String rangeString) {
validateRangeString(rangeString);
Inclusiveness minVersionInclusiveness =
rangeString.startsWith("[") ? Inclusiveness.INCLUSIVE : Inclusiveness.EXCLUSIVE;
Inclusiveness maxVersionInclusiveness =
rangeString.endsWith("]") ? Inclusiveness.INCLUSIVE : Inclusiveness.EXCLUSIVE;
int commaIndex = rangeString.indexOf(',');
String minVersionString = rangeString.substring(1, commaIndex).trim();
Version minVersion;
if (minVersionString.isEmpty()) {
minVersionInclusiveness = Inclusiveness.EXCLUSIVE;
minVersion = Version.minimum();
} else {
minVersion = Version.fromString(minVersionString);
}
String maxVersionString =
rangeString.substring(commaIndex + 1, rangeString.length() - 1).trim();
Version maxVersion;
if (maxVersionString.isEmpty()) {
maxVersionInclusiveness = Inclusiveness.EXCLUSIVE;
maxVersion = Version.maximum();
} else {
maxVersion = Version.fromString(maxVersionString);
}
if (!minVersion.isLessThan(maxVersion)) {
throw new IllegalArgumentException(
String.format(
"Min version in range must be less than max version in range, got '%s'",
rangeString));
}
return builder()
.setMinVersion(minVersion)
.setMinVersionInclusiveness(minVersionInclusiveness)
.setMaxVersion(maxVersion)
.setMaxVersionInclusiveness(maxVersionInclusiveness)
.build();
}
public static boolean isValidVersionRange(String rangeString) {
try {
parse(rangeString);
return true;
} catch (Throwable t) {
return false;
}
}
private static void validateRangeString(String rangeString) {
checkArgument(!Strings.isNullOrEmpty(rangeString), "Range string cannot be empty.");
// Version range string must start with '[' or '('.
if (!rangeString.startsWith("[") && !rangeString.startsWith("(")) {
throw new IllegalArgumentException(
String.format("Version range must start with '[' or '(', got '%s'", rangeString));
}
// Version range string must end with ']' or ')'.
if (!rangeString.endsWith("]") && !rangeString.endsWith(")")) {
throw new IllegalArgumentException(
String.format("Version range must end with ']' or ')', got '%s'", rangeString));
}
// Remove the leading and ending parenthesis and brackets.
String trimmedRange = rangeString.substring(1, rangeString.length() - 1).trim();
// No more parenthesis and brackets in the string.
if (CharMatcher.anyOf("[()]").matchesAnyOf(trimmedRange)) {
throw new IllegalArgumentException(
String.format(
"Parenthesis and/or brackets not allowed within version range, got '%s'",
rangeString));
}
// Only one comma that separates the minimum and maximum.
if (CharMatcher.is(',').countIn(trimmedRange) != 1) {
throw new IllegalArgumentException(
String.format("Invalid range of versions, got '%s'", rangeString));
}
// Version range of minimum to maximum is not supported.
if (trimmedRange.equals(",")) {
throw new IllegalArgumentException(
String.format("Infinity range is not supported, got '%s'", rangeString));
}
}
}
================================================
FILE: common/src/main/java/com/google/tsunami/common/version/VersionSet.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
/** Immutable set of discrete versions and version ranges. */
@AutoValue
@Immutable
public abstract class VersionSet {
public abstract ImmutableList versions();
public abstract ImmutableList versionRanges();
public static Builder builder() {
return new AutoValue_VersionSet.Builder();
}
/** Builder for {@link VersionSet}. */
@AutoValue.Builder
public abstract static class Builder {
abstract ImmutableList.Builder versionsBuilder();
public Builder addVersion(Version version) {
versionsBuilder().add(version);
return this;
}
abstract ImmutableList.Builder versionRangesBuilder();
public Builder addVersionRange(VersionRange versionRange) {
versionRangesBuilder().add(versionRange);
return this;
}
public abstract VersionSet build();
}
public static VersionSet parse(ImmutableList versionAndRangesList) {
checkNotNull(versionAndRangesList);
checkArgument(!versionAndRangesList.isEmpty(), "Versions and ranges list cannot be empty.");
VersionSet.Builder versionSetBuilder = VersionSet.builder();
for (String versionOrRangeString : versionAndRangesList) {
if (isDiscreteVersion(versionOrRangeString)) {
versionSetBuilder.addVersion(Version.fromString(versionOrRangeString));
} else if (VersionRange.isValidVersionRange(versionOrRangeString)) {
versionSetBuilder.addVersionRange(VersionRange.parse(versionOrRangeString));
} else {
throw new IllegalArgumentException(
String.format(
"String '%s' is neither a discrete string nor a version range.",
versionOrRangeString));
}
}
return versionSetBuilder.build();
}
private static boolean isDiscreteVersion(String versionOrRangeString) {
return CharMatcher.anyOf("[()], ").matchesNoneOf(versionOrRangeString);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/cli/CliOptionsModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.cli;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link CliOptionsModule}. */
@RunWith(JUnit4.class)
public class CliOptionsModuleTest {
@Test
public void configure_whenValidArgs_parsesSuccessfully() {
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(
TestOption.class.getTypeName(), TestOptionWithRequiredParam.class.getTypeName())
.scan()) {
Injector injector =
Guice.createInjector(
new CliOptionsModule(
scanResult, "test", new String[] {"--test=testoption", "--test_required=abc"}));
TestOption testOption = injector.getInstance(TestOption.class);
TestOptionWithRequiredParam testOptionWithRequiredParam =
injector.getInstance(TestOptionWithRequiredParam.class);
assertThat(testOption.test).isEqualTo("testoption");
assertThat(testOptionWithRequiredParam.testRequired).isEqualTo("abc");
}
}
@Test
public void configure_whenMissingRequiredArgs_throwsException() {
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(
TestOption.class.getTypeName(), TestOptionWithRequiredParam.class.getTypeName())
.scan()) {
CreationException ex =
assertThrows(
CreationException.class,
() ->
Guice.createInjector(
new CliOptionsModule(scanResult, "test", new String[] {"--test=test"})));
assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class);
}
}
@Test
public void configure_whenInvalidArgs_throwsException() {
try (ScanResult scanResult =
new ClassGraph().enableAllInfo().whitelistClasses(TestOption.class.getTypeName()).scan()) {
CreationException ex =
assertThrows(
CreationException.class,
() ->
Guice.createInjector(
new CliOptionsModule(scanResult, "test", new String[] {"--test=invalid"})));
assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class);
}
}
@Test
public void configure_whenUnknownArgs_throwsException() {
try (ScanResult scanResult =
new ClassGraph().enableAllInfo().whitelistClasses(TestOption.class.getTypeName()).scan()) {
CreationException ex =
assertThrows(
CreationException.class,
() ->
Guice.createInjector(
new CliOptionsModule(
scanResult, "test", new String[] {"--test=test", "--unknown=unknown"})));
assertThat(ex).hasCauseThat().isInstanceOf(ParameterException.class);
}
}
@Test
public void configure_whenCliOptionNoCorrectCtor_throwsException() {
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(TestOptionWithoutNoArgumentCtor.class.getTypeName())
.scan()) {
assertThrows(
AssertionError.class,
() ->
Guice.createInjector(
new CliOptionsModule(scanResult, "test", new String[] {})));
}
}
@Parameters(separators = "=")
private static final class TestOption implements CliOption {
@Parameter(names = "--test", description = "A test option")
String test;
@Override
public void validate() {
if (Strings.isNullOrEmpty(test)) {
throw new ParameterException("Empty test param");
}
if (!test.startsWith("test")) {
throw new ParameterException("test param value must start with 'test'");
}
}
}
@Parameters(separators = "=")
private static final class TestOptionWithRequiredParam implements CliOption {
@Parameter(names = "--test_required", description = "A required option", required = true)
String testRequired;
@Override
public void validate() {}
}
@Parameters(separators = "=")
private static final class TestOptionWithoutNoArgumentCtor implements CliOption {
String testOption;
TestOptionWithoutNoArgumentCtor(String testOption) {
this.testOption = testOption;
}
@Override
public void validate() {}
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/command/CommandExecutorFactoryTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
/** Tests for {@link CommandExecutorFactory}. */
@RunWith(JUnit4.class)
public final class CommandExecutorFactoryTest {
@Test
public void getInstance_whenNoPreviousInstanceIsProvided_createsNewProcessExecutor() {
CommandExecutor executor = CommandExecutorFactory.create("fakeArgs");
assertThat(executor).isNotNull();
}
@Test
public void getInstance_whenPreviousInstanceIsProvided_returnsProvidedInstance() {
CommandExecutor executor = Mockito.mock(CommandExecutor.class);
CommandExecutorFactory.setInstance(executor);
assertThat(CommandExecutorFactory.create("fakeArgs")).isSameInstanceAs(executor);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/command/CommandExecutorTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.command;
import static com.google.common.truth.Truth.assertThat;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link CommandExecutor}. */
@RunWith(JUnit4.class)
public final class CommandExecutorTest {
@Test
public void execute_always_startsProcessAndReturnsProcessInstance()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1");
Process process = executor.execute();
process.waitFor();
assertThat(process.exitValue()).isEqualTo(0);
}
@Test
public void executeAsync_always_startsProcessAndReturnsProcessInstance()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1");
Process process = executor.executeAsync();
process.waitFor();
assertThat(process.exitValue()).isEqualTo(0);
}
@Test
public void executeWithNoStreamCollection_always_startsProcessAndReturnsProcessInstance()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1");
Process process = executor.executeWithNoStreamCollection();
process.waitFor();
assertThat(process.exitValue()).isEqualTo(0);
}
@Test
public void getOutput_always_returnsExpect()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1");
Process process = executor.execute();
process.waitFor();
assertThat(executor.getOutput()).isEqualTo("1\n");
}
@Test
public void getOutput_withMultipleGetOutputCalls_returnsExpect()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1");
Process process = executor.execute();
process.waitFor();
assertThat(executor.getOutput()).isEqualTo("1\n");
assertThat(executor.getOutput()).isEqualTo("1\n");
}
@Test
public void getError_always_returnsExpect()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1 1>&2");
Process process = executor.execute();
process.waitFor();
assertThat(executor.getError()).isEqualTo("1\n");
}
@Test
public void getError_withMultipleGetOutputCalls_returnsExpect()
throws IOException, InterruptedException, ExecutionException {
CommandExecutor executor = new CommandExecutor("/bin/sh", "-c", "echo 1 1>&2");
Process process = executor.execute();
process.waitFor();
assertThat(executor.getError()).isEqualTo("1\n");
assertThat(executor.getError()).isEqualTo("1\n");
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/concurrent/BaseThreadPoolModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static org.junit.Assert.assertThrows;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link ThreadPoolModule}. */
@RunWith(JUnit4.class)
public final class BaseThreadPoolModuleTest {
/** Internal annotation used for tests. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@interface TestThreadPool {}
static final class TestThreadPoolModule extends BaseThreadPoolModule {
TestThreadPoolModule(Builder builder) {
super(builder);
}
@Override
void configureThreadPool(Key key) {}
static final class Builder
extends BaseThreadPoolModuleBuilder {
Builder() {
super(ListeningExecutorService.class);
}
@Override
Builder self() {
return this;
}
@Override
void validate() {}
@Override
AbstractModule newModule() {
return new TestThreadPoolModule(this);
}
}
}
@Test
public void build_whenNoName_throwsIllegalStateException() {
assertThrows(IllegalStateException.class, () -> new TestThreadPoolModule.Builder().build());
}
@Test
public void build_whenEmptyName_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> new TestThreadPoolModule.Builder().setName("").build());
}
@Test
public void build_whenNegativeCoreSize_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> new TestThreadPoolModule.Builder().setName("test").setCoreSize(-1).build());
}
@Test
public void build_whenNegativeMaxSize_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> new TestThreadPoolModule.Builder().setName("test").setMaxSize(-1).build());
}
@Test
public void build_whenNegativeKeepAliveSeconds_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
new TestThreadPoolModule.Builder()
.setName("test")
.setMaxSize(1)
.setKeepAliveSeconds(-1)
.build());
}
@Test
public void build_whenCoreSizeLessThanMaxSize_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
new TestThreadPoolModule.Builder()
.setName("test")
.setCoreSize(2)
.setMaxSize(1)
.setAnnotation(TestThreadPool.class)
.build());
}
@Test
public void build_whenNoAnnotation_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() -> new TestThreadPoolModule.Builder().setName("test").build());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/concurrent/ScheduledThreadPoolModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Qualifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link ScheduledThreadPoolModule}. */
@RunWith(JUnit4.class)
public final class ScheduledThreadPoolModuleTest {
/** Internal annotation used for tests. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@interface TestThreadPool {}
@Test
public void configure_always_bindsListeningScheduledExecutorService() {
Injector injector =
Guice.createInjector(
new ScheduledThreadPoolModule.Builder()
.setName("test")
.setSize(1)
.setAnnotation(TestThreadPool.class)
.build());
assertThat(
injector.getInstance(Key.get(ScheduledExecutorService.class, TestThreadPool.class)))
.isInstanceOf(ListeningScheduledExecutorService.class);
}
@Test
public void configure_always_bindsSingleton() {
Injector injector =
Guice.createInjector(
new ScheduledThreadPoolModule.Builder()
.setName("test")
.setSize(1)
.setAnnotation(TestThreadPool.class)
.build());
ScheduledExecutorService scheduledExecutorService =
injector.getInstance(Key.get(ScheduledExecutorService.class, TestThreadPool.class));
ListeningScheduledExecutorService listeningScheduledExecutorService =
injector.getInstance(
Key.get(ListeningScheduledExecutorService.class, TestThreadPool.class));
assertThat(scheduledExecutorService).isSameInstanceAs(listeningScheduledExecutorService);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/concurrent/ThreadPoolModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.concurrent;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.PriorityBlockingQueue;
import javax.inject.Qualifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link ThreadPoolModule}. */
@RunWith(JUnit4.class)
public final class ThreadPoolModuleTest {
/** Internal annotation used for tests. */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@interface TestThreadPool {}
@Test
public void configure_always_bindsListeningExecutorService() {
Injector injector =
Guice.createInjector(
new ThreadPoolModule.Builder()
.setName("test")
.setCoreSize(1)
.setMaxSize(1)
.setAnnotation(TestThreadPool.class)
.build());
assertThat(injector.getInstance(Key.get(Executor.class, TestThreadPool.class)))
.isInstanceOf(ListeningExecutorService.class);
assertThat(injector.getInstance(Key.get(ExecutorService.class, TestThreadPool.class)))
.isInstanceOf(ListeningExecutorService.class);
}
@Test
public void configure_always_bindsSingleton() {
Injector injector =
Guice.createInjector(
new ThreadPoolModule.Builder()
.setName("test")
.setCoreSize(1)
.setMaxSize(1)
.setAnnotation(TestThreadPool.class)
.build());
Executor executor = injector.getInstance(Key.get(Executor.class, TestThreadPool.class));
ExecutorService executorService =
injector.getInstance(Key.get(ExecutorService.class, TestThreadPool.class));
ListeningExecutorService listeningExecutorService =
injector.getInstance(Key.get(ListeningExecutorService.class, TestThreadPool.class));
assertThat(executor).isSameInstanceAs(listeningExecutorService);
assertThat(executorService).isSameInstanceAs(listeningExecutorService);
}
@Test
public void build_whenNegativeQueueCapacity_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
new ThreadPoolModule.Builder()
.setName("test")
.setMaxSize(1)
.setQueueCapacity(-1)
.build());
}
@Test
public void build_whenBothQueueCapacityAndBlockingQueueSet_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
new ThreadPoolModule.Builder()
.setMaxSize(1)
.setQueueCapacity(1)
.setBlockingQueue(new PriorityBlockingQueue<>(1))
.build());
}
@Test
public void build_whenUnBoundedQueue_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
new ThreadPoolModule.Builder()
.setName("test")
.setCoreSize(1)
.setMaxSize(2)
.setAnnotation(TestThreadPool.class)
.build());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/config/ConfigModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.tsunami.common.config.annotations.ConfigProperties;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link ConfigModule}. */
@RunWith(JUnit4.class)
public final class ConfigModuleTest {
@Test
public void configure_always_bindsGivenTsunamiConfigObject() {
TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of());
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(TestConfigWithoutPrefix.class.getTypeName())
.scan()) {
Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig));
TsunamiConfig boundTsunamiConfig = injector.getInstance(TsunamiConfig.class);
assertThat(boundTsunamiConfig).isSameInstanceAs(tsunamiConfig);
}
}
@Test
public void configure_whenValidConfigData_bindsSuccessfully() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(ImmutableMap.of("string_config", "testString"));
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(TestConfigWithoutPrefix.class.getTypeName())
.scan()) {
Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig));
TestConfigWithoutPrefix testConfig = injector.getInstance(TestConfigWithoutPrefix.class);
assertThat(testConfig.stringConfig).isEqualTo("testString");
}
}
@Test
public void configure_whenMissingMatchedConfigData_bindsObjectWithDefaultValue() {
TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of());
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(TestConfigWithoutPrefix.class.getTypeName())
.scan()) {
Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig));
TestConfigWithoutPrefix testConfig = injector.getInstance(TestConfigWithoutPrefix.class);
assertThat(testConfig.stringConfig).isNull();
}
}
@Test
public void configure_whenValidConfigDataWithPrefix_bindsSuccessfully() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of(
"test", ImmutableMap.of("prefix", ImmutableMap.of("string_config", "testString"))));
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(TestConfigWithPrefix.class.getTypeName())
.scan()) {
Injector injector = Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig));
TestConfigWithPrefix testConfig = injector.getInstance(TestConfigWithPrefix.class);
assertThat(testConfig.stringConfig).isEqualTo("testString");
}
}
@Test
public void configure_whenInvalidConfigClass_throwsException() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(ImmutableMap.of("string_config", "testString"));
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.whitelistClasses(InvalidConfig.class.getTypeName())
.scan()) {
assertThrows(
AssertionError.class,
() -> Guice.createInjector(new ConfigModule(scanResult, tsunamiConfig)));
}
}
@ConfigProperties("")
private static final class TestConfigWithoutPrefix {
String stringConfig;
}
@ConfigProperties("test.prefix")
private static final class TestConfigWithPrefix {
String stringConfig;
}
@ConfigProperties("")
private static final class InvalidConfig {
String stringConfig;
InvalidConfig(String stringConfig) {
this.stringConfig = stringConfig;
}
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/config/TsunamiConfigTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link TsunamiConfig}. */
@RunWith(JUnit4.class)
public final class TsunamiConfigTest {
private static final String TEST_PROPERTY = "test.property";
@After
public void tearDown() {
System.clearProperty(TEST_PROPERTY);
}
@Test
public void fromYamlData_always_createTsunamiConfigFromMapData() {
TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(ImmutableMap.of("test", "value"));
assertThat(tsunamiConfig.getRawConfigData()).containsEntry("test", "value");
}
@Test
public void fromYamlData_whenNullYamlData_createEmptyConfigData() {
TsunamiConfig tsunamiConfig = TsunamiConfig.fromYamlData(null);
assertThat(tsunamiConfig.getRawConfigData()).isEmpty();
}
@Test
public void getSystemProperty_whenPropertyExists_returnsPropertyValue() {
System.setProperty(TEST_PROPERTY, "Test value");
assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY)).hasValue("Test value");
}
@Test
public void getSystemProperty_whenPropertyNotExists_returnsEmptyOptional() {
assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY)).isEmpty();
}
@Test
public void getSystemProperty_whenPropertyNotExistsWithDefaultValue_returnsDefaultValue() {
assertThat(TsunamiConfig.getSystemProperty(TEST_PROPERTY, "Default")).isEqualTo("Default");
}
@Test
public void getConfig_whenValidConfigData_returnsBoundConfigObject() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(ImmutableMap.of("string", "string_value", "number", 1234));
SimpleConfig config = tsunamiConfig.getConfig("", SimpleConfig.class);
assertThat(config.string).isEqualTo("string_value");
assertThat(config.number).isEqualTo(1234);
}
@Test
public void getConfig_whenValidConfigDataAndPrefix_returnsBoundConfigObject() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of(
"test",
ImmutableMap.of(
"prefix", ImmutableMap.of("string", "string_value", "number", 1234))));
SimpleConfig config = tsunamiConfig.getConfig("test.prefix", SimpleConfig.class);
assertThat(config.string).isEqualTo("string_value");
assertThat(config.number).isEqualTo(1234);
}
@Test
public void getConfig_whenRequestedConfigNotExists_returnsObjectWithDefaultValue() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of(
"test",
ImmutableMap.of(
"prefix", ImmutableMap.of("string", "string_value", "number", 1234))));
SimpleConfig config = tsunamiConfig.getConfig("not.exist.prefix", SimpleConfig.class);
assertThat(config.string).isNull();
assertThat(config.number).isEqualTo(0);
}
@Test
public void getConfig_whenConfigHasComplicateDataStructure_returnsValidObject() {
ImmutableList stringsField = ImmutableList.of("a", "b", "c");
ImmutableMap> complicateField =
ImmutableMap.of("keyA", ImmutableList.of(1L, 2L), "keyB", ImmutableList.of(123L));
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of(
"strings", stringsField, "complicateField", complicateField));
CollectionConfig config = tsunamiConfig.getConfig("", CollectionConfig.class);
assertThat(config.strings).isEqualTo(stringsField);
assertThat(config.complicateField).isEqualTo(complicateField);
}
@Test
public void getConfig_whenConfigDataUseLowerUnderscoreCase_returnsValidObject() {
ImmutableList stringsField = ImmutableList.of("a", "b", "c");
ImmutableMap> complicateField =
ImmutableMap.of("keyA", ImmutableList.of(1L, 2L), "keyB", ImmutableList.of(123L));
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of(
"strings", stringsField, "complicate_field", complicateField));
CollectionConfig config = tsunamiConfig.getConfig("", CollectionConfig.class);
assertThat(config.strings).isEqualTo(stringsField);
assertThat(config.complicateField).isEqualTo(complicateField);
}
@Test
public void getConfig_whenRequestedConfigHasInvalidType_throwsException() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of("test", ImmutableMap.of("prefix", "invalid_type")));
assertThrows(
ConfigException.class, () -> tsunamiConfig.getConfig("test.prefix", SimpleConfig.class));
}
@Test
public void getConfig_whenUnassignableConfigValue_throwsException() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(
ImmutableMap.of("string", "string_value", "number", "incompatible_value"));
assertThrows(
IllegalArgumentException.class, () -> tsunamiConfig.getConfig("", SimpleConfig.class));
}
@Test
public void getConfig_whenInvalidConfigObject_throwsException() {
TsunamiConfig tsunamiConfig =
TsunamiConfig.fromYamlData(ImmutableMap.of("string", "string_value"));
assertThrows(AssertionError.class, () -> tsunamiConfig.getConfig("", InvalidConfig.class));
}
private static final class SimpleConfig {
String string;
long number;
}
private static final class CollectionConfig {
List strings;
Map> complicateField;
}
private static final class InvalidConfig {
String field;
InvalidConfig(String field) {
this.field = field;
}
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/config/YamlConfigLoaderTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.config;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link YamlConfigLoader}. */
@RunWith(JUnit4.class)
public final class YamlConfigLoaderTest {
private static final String YAML_DATA = "test: \"data\"\ntest2: 123";
@After
public void tearDown() {
System.clearProperty("tsunami.config.location");
}
@Test
public void loadConfig_whenValidYamlFile_loadsConfigFromFile() throws IOException {
File configFile = File.createTempFile("YamlConfigLoaderTest", ".yaml");
Files.asCharSink(configFile, UTF_8).write(YAML_DATA);
System.setProperty("tsunami.config.location", configFile.getAbsolutePath());
TsunamiConfig tsunamiConfig = new YamlConfigLoader().loadConfig();
assertThat(tsunamiConfig.getRawConfigData()).containsExactly("test", "data", "test2", 123);
}
@Test
public void loadConfig_whenYamlFileNotFound_usesEmptyConfig() {
TsunamiConfig tsunamiConfig = new YamlConfigLoader().loadConfig();
assertThat(tsunamiConfig.getRawConfigData()).isEmpty();
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/data/NetworkEndpointUtilsTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.data;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.net.HostAndPort;
import com.google.tsunami.proto.AddressFamily;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.IpAddress;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.Port;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link NetworkEndpointUtils}. */
@RunWith(JUnit4.class)
public class NetworkEndpointUtilsTest {
@Test
public void isIpV6Endpoint_withIpV4Endpoint_returnsFalse() {
NetworkEndpoint ipV4Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV4Endpoint)).isFalse();
}
@Test
public void isIpV6Endpoint_withIpV4AndPortEndpoint_returnsFalse() {
NetworkEndpoint ipV4AndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV4AndPortEndpoint)).isFalse();
}
@Test
public void isIpV6Endpoint_withIpV6Endpoint_returnsFalse() {
NetworkEndpoint ipV6Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV6Endpoint)).isTrue();
}
@Test
public void isIpV6Endpoint_withIpV6AndPortEndpoint_returnsFalse() {
NetworkEndpoint ipV6AndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(ipV6AndPortEndpoint)).isTrue();
}
@Test
public void isIpV6Endpoint_withHostnameEndpoint_returnsFalse() {
NetworkEndpoint hostnameEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME)
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(hostnameEndpoint)).isFalse();
}
@Test
public void isIpV6Endpoint_withHostnameAndPortEndpoint_returnsFalse() {
NetworkEndpoint hostnameAndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.isIpV6Endpoint(hostnameAndPortEndpoint)).isFalse();
}
@Test
public void toUriString_withIpV4Endpoint_returnsIpAddress() {
NetworkEndpoint ipV4Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(ipV4Endpoint)).isEqualTo("1.2.3.4");
}
@Test
public void toUriString_withIpV6Endpoint_returnsIpAddressWithBracket() {
NetworkEndpoint ipV6Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(ipV6Endpoint)).isEqualTo("[3ffe::1]");
}
@Test
public void toUriString_withIpV4AndPortEndpoint_returnsIpAddressAndPort() {
NetworkEndpoint ipV4AndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(ipV4AndPortEndpoint)).isEqualTo("1.2.3.4:8888");
}
@Test
public void toUriString_withIpV6AndPortEndpoint_returnsIpAddressWithBracketAndPort() {
NetworkEndpoint ipV6Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder().setAddress("3ffe::1").setAddressFamily(AddressFamily.IPV6))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(ipV6Endpoint)).isEqualTo("[3ffe::1]:8888");
}
@Test
public void toUriString_withHostnameEndpoint_returnsHostname() {
NetworkEndpoint hostnameEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME)
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(hostnameEndpoint)).isEqualTo("localhost");
}
@Test
public void toUriString_withHostnameAndPortEndpoint_returnsHostnameAndPort() {
NetworkEndpoint hostnameAndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.toUriAuthority(hostnameAndPortEndpoint))
.isEqualTo("localhost:8888");
}
@Test
public void toHostAndPort_withIpAddress_returnsHostWithIp() {
NetworkEndpoint ipV4Endpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.toHostAndPort(ipV4Endpoint))
.isEqualTo(HostAndPort.fromHost("1.2.3.4"));
}
@Test
public void toHostAndPort_withIpAddressAndPort_returnsHostWithIpAndPort() {
NetworkEndpoint ipV4AndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder().setAddress("1.2.3.4").setAddressFamily(AddressFamily.IPV4))
.build();
assertThat(NetworkEndpointUtils.toHostAndPort(ipV4AndPortEndpoint))
.isEqualTo(HostAndPort.fromParts("1.2.3.4", 8888));
}
@Test
public void toHostAndPort_withHostname_returnsHostWithHostname() {
NetworkEndpoint hostnameEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME)
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.toHostAndPort(hostnameEndpoint))
.isEqualTo(HostAndPort.fromHost("localhost"));
}
@Test
public void toHostAndPort_withHostnameAndPort_returnsHostWithHostnameAndPort() {
NetworkEndpoint hostnameAndPortEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
assertThat(NetworkEndpointUtils.toHostAndPort(hostnameAndPortEndpoint))
.isEqualTo(HostAndPort.fromParts("localhost", 8888));
}
@Test
public void forIp_withIpV4Address_returnsIpV4NetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIp("1.2.3.4"))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV4)
.setAddress("1.2.3.4"))
.build());
}
@Test
public void forIp_withIpV6Address_returnsIpV6NetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIp("3ffe::1"))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP)
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV6)
.setAddress("3ffe::1"))
.build());
}
@Test
public void forIpAndPort_withIpV4AddressAndPort_returnsIpV4AndPortNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIpAndPort("1.2.3.4", 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV4)
.setAddress("1.2.3.4"))
.build());
}
@Test
public void forIpAndPort_withIpV6AddressAndPort_returnsIpV6AndPortNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIpAndPort("3ffe::1", 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV6)
.setAddress("3ffe::1"))
.build());
}
@Test
public void forHostname_withHostname_returnsHostnameNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forHostname("localhost"))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME)
.setHostname(Hostname.newBuilder().setName("localhost"))
.build());
}
@Test
public void forHostnameAndPort_withHostnameAndPort_returnsHostnameAndPortNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forHostnameAndPort("localhost", 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build());
}
@Test
public void forIpAndHostname_returnsIpAndHostnameNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIpAndHostname("1.2.3.4", "host.com"))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME)
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV4)
.setAddress("1.2.3.4"))
.setHostname(Hostname.newBuilder().setName("host.com"))
.build());
}
@Test
public void forIpHostnameAndPort_returnsIpHostnameAndPortNetworkEndpoint() {
assertThat(NetworkEndpointUtils.forIpHostnameAndPort("1.2.3.4", "host.com", 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV4)
.setAddress("1.2.3.4"))
.setHostname(Hostname.newBuilder().setName("host.com"))
.setPort(Port.newBuilder().setPortNumber(8888))
.build());
}
@Test
public void forNetworkEndpointAndPort_withIpEndpointAndPort_returnsIpAndPort() {
NetworkEndpoint ipEndpoint = NetworkEndpointUtils.forIp("1.2.3.4");
assertThat(NetworkEndpointUtils.forNetworkEndpointAndPort(ipEndpoint, 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setIpAddress(
IpAddress.newBuilder()
.setAddressFamily(AddressFamily.IPV4)
.setAddress("1.2.3.4"))
.build());
}
@Test
public void forNetworkEndpointAndPort_withHostnameEndpointAndPort_returnsHostnameAndPort() {
NetworkEndpoint hostnameEndpoint = NetworkEndpointUtils.forHostname("localhost");
assertThat(NetworkEndpointUtils.forNetworkEndpointAndPort(hostnameEndpoint, 8888))
.isEqualTo(
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.HOSTNAME_PORT)
.setPort(Port.newBuilder().setPortNumber(8888))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build());
}
@Test
public void forIp_withInvalidIp_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forIp("abc"));
}
@Test
public void forIpAndPort_withInvalidIp_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", 8888));
}
@Test
public void forIpAndPort_withInvalidPort_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", -1));
assertThrows(
IllegalArgumentException.class, () -> NetworkEndpointUtils.forIpAndPort("abc", 65536));
}
@Test
public void forHostname_withIpAddress_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostname("1.2.3.4"));
assertThrows(IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostname("3ffe::1"));
}
@Test
public void forHostnameAndPort_withIpAddress_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> NetworkEndpointUtils.forHostnameAndPort("1.2.3.4", 8888));
assertThrows(
IllegalArgumentException.class,
() -> NetworkEndpointUtils.forHostnameAndPort("3ffe::1", 8888));
}
@Test
public void forHostnameAndPort_withInvalidPort_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> NetworkEndpointUtils.forHostnameAndPort("abc", -1));
assertThrows(
IllegalArgumentException.class,
() -> NetworkEndpointUtils.forHostnameAndPort("abc", 65536));
}
@Test
public void forNetworkEndpointAndPort_withInvalidEndpointType_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
NetworkEndpointUtils.forNetworkEndpointAndPort(
NetworkEndpointUtils.forIpAndPort("1.2.3.4", 80), 8888));
assertThrows(
IllegalArgumentException.class,
() ->
NetworkEndpointUtils.forNetworkEndpointAndPort(
NetworkEndpointUtils.forHostnameAndPort("localhost", 80), 8888));
}
@Test
public void forNetworkEndpointAndPort_withInvalidPort_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
NetworkEndpointUtils.forNetworkEndpointAndPort(
NetworkEndpointUtils.forIp("1.2.3.4"), -1));
assertThrows(
IllegalArgumentException.class,
() ->
NetworkEndpointUtils.forNetworkEndpointAndPort(
NetworkEndpointUtils.forHostname("localhost"), 65536));
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/data/NetworkServiceUtilsTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.data;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndPort;
import com.google.tsunami.proto.AddressFamily;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.IpAddress;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.NetworkService;
import com.google.tsunami.proto.Port;
import com.google.tsunami.proto.ServiceContext;
import com.google.tsunami.proto.Software;
import com.google.tsunami.proto.TransportProtocol;
import com.google.tsunami.proto.WebServiceContext;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URL;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link NetworkServiceUtils}. */
@RunWith(JUnit4.class)
public final class NetworkServiceUtilsTest {
@Test
public void isWebService_whenHttpService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("http").build()))
.isTrue();
}
@Test
public void isWebService_whenHttpAltService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("http-alt").build()))
.isTrue();
}
@Test
public void isWebService_whenHttpProxyService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("http-proxy").build()))
.isTrue();
}
@Test
public void isWebService_whenHttpsService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("https").build()))
.isTrue();
}
@Test
public void isWebService_whenRadanHttpService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("radan-http").build()))
.isTrue();
}
@Test
public void isWebService_whenSslHttpService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("ssl/http").build()))
.isTrue();
}
@Test
public void isWebService_whenSslHttpsService_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("ssl/https").build()))
.isTrue();
}
@Test
public void isWebService_whenCapitalizedHttpService_ignoresCaseAndReturnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("HTTP").build()))
.isTrue();
}
@Test
public void isWebService_whenHasAtLeastOneHttpMethod_returnsTrue() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder()
.setServiceName("irrelevantService")
.addSupportedHttpMethods("IrrelevantMethodName")
.build()))
.isTrue();
}
@Test
public void isWebService_whenNonWebService_returnsFalse() {
assertThat(
NetworkServiceUtils.isWebService(
NetworkService.newBuilder().setServiceName("ssh").build()))
.isFalse();
}
@Test
public void isPlainHttp_whenPlainHttpService_returnsTrue() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder().setServiceName("http").build()))
.isTrue();
}
@Test
public void isPlainHttp_whenHttpAltService_returnsTrue() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder().setServiceName("http-alt").build()))
.isTrue();
}
@Test
public void isPlainHttp_whenHttpsService_returnsFalse() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder().setServiceName("https").build()))
.isFalse();
}
@Test
public void isPlainHttp_whenRadanHttpService_returnsTrue() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder().setServiceName("radan-http").build()))
.isTrue();
}
@Test
public void isPlainHttp_whenNonWebService_returnsFalse() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder().setServiceName("ssh").build()))
.isFalse();
}
@Test
public void isPlainHttp_whenHttpServiceButHasSslVersions_returnsFalse() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder()
.setServiceName("http")
.addSupportedSslVersions("SSLV3")
.build()))
.isFalse();
}
@Test
public void isPlainHttp_whenNonHttpServiceButHasSslVersions_returnsFalse() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder()
.setServiceName("ssh")
.addSupportedSslVersions("SSLV3")
.build()))
.isFalse();
}
@Test
public void isPlainHttp_whenHttpServiceFromHttpMethodsWithoutSslVersions_returnsTrue() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder()
.setServiceName("ssh")
.addSupportedHttpMethods("GET")
.build()))
.isTrue();
}
@Test
public void isPlainHttp_whenHttpServiceWithSslVersions_returnsFalse() {
assertThat(
NetworkServiceUtils.isPlainHttp(
NetworkService.newBuilder()
.setServiceName("http")
.addSupportedSslVersions("SSLV3")
.build()))
.isFalse();
}
@Test
public void getServiceName_whenNonWebService_returnsServiceName() {
assertThat(
NetworkServiceUtils.getServiceName(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 22))
.setServiceName("ssh")
.build()))
.isEqualTo("ssh");
}
@Test
public void getServiceName_whenWebServiceNoSoftware_returnsServiceName() {
assertThat(
NetworkServiceUtils.getServiceName(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 22))
.setServiceName("http")
.build()))
.isEqualTo("http");
}
@Test
public void getServiceName_whenWebServiceWithSoftware_returnsServiceName() {
assertThat(
NetworkServiceUtils.getServiceName(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 22))
.setServiceName("http")
.setSoftware(Software.newBuilder().setName("WordPress"))
.build()))
.isEqualTo("wordpress");
}
@Test
public void getWebServiceName_whenWebServiceWithSoftware_returnsWebServiceName() {
assertThat(
NetworkServiceUtils.getWebServiceName(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080))
.setServiceName("http")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder()
.setSoftware(Software.newBuilder().setName("jenkins"))))
.build()))
.isEqualTo("jenkins");
}
@Test
public void getServiceName_whenWebServiceNoContext_returnsServiceName() {
assertThat(
NetworkServiceUtils.getWebServiceName(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080))
.setServiceName("http")
.setSoftware(Software.newBuilder().setName("nothttp"))
.build()))
.isEqualTo("http");
}
@Test
public void buildWebApplicationRootUrl_whenHttpWithoutRoot_buildsExpectedUrl() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080))
.setServiceName("http")
.build()))
.isEqualTo("http://127.0.0.1:8080/");
}
@Test
public void buildWebApplicationRootUrl_whenHttpsWithoutRoot_buildsExpectedUrl() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8443))
.setServiceName("ssl/https")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("test_root")))
.build()))
.isEqualTo("https://127.0.0.1:8443/test_root/");
}
@Test
public void buildWebApplicationRootUrl_whenHttpWithRootPath_buildsUrlWithExpectedRoot() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080))
.setServiceName("http")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("/test_root")))
.build()))
.isEqualTo("http://127.0.0.1:8080/test_root/");
}
@Test
public void buildWebApplicationRootUrl_whenRootPathNoLeadingSlash_appendsLeadingSlash() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 8080))
.setServiceName("http")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("test_root")))
.build()))
.isEqualTo("http://127.0.0.1:8080/test_root/");
}
@Test
public void buildWebApplicationRootUrl_whenHttpServiceOnPort80_removesTrailingPortFromUrl() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 80))
.setServiceName("http")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("test_root")))
.build()))
.isEqualTo("http://127.0.0.1/test_root/");
}
@Test
public void buildWebApplicationRootUrl_whenHttpsServiceOnPort443_removesTrailingPortFromUrl() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 443))
.setServiceName("ssl/https")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("test_root")))
.build()))
.isEqualTo("https://127.0.0.1/test_root/");
}
@Test
public void buildWebApplicationRootUrl_whenNotWebService_returnsHttpUrl() {
assertThat(
NetworkServiceUtils.buildWebApplicationRootUrl(
NetworkService.newBuilder()
.setNetworkEndpoint(forIpAndPort("127.0.0.1", 2121))
.setServiceName("unknown")
.build()))
.isEqualTo("http://127.0.0.1:2121/");
}
@Test
public void buildUriNetworkService_returnsNetworkService() throws IOException {
URL url = new URL("https://localhost/function1");
String hostname = url.getHost();
String ipaddress = InetAddress.getByName(hostname).getHostAddress();
InetAddress inetAddress = InetAddress.getByName(url.getHost());
AddressFamily addressFamily =
inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6;
NetworkEndpoint networkEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setIpAddress(
IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipaddress))
.setPort(Port.newBuilder().setPortNumber(443))
.setHostname(Hostname.newBuilder().setName("localhost"))
.build();
NetworkService networkService =
NetworkService.newBuilder()
.setNetworkEndpoint(networkEndpoint)
.setTransportProtocol(TransportProtocol.TCP)
.setServiceName("https")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setApplicationRoot("/function1")))
.build();
assertThat(NetworkServiceUtils.buildUriNetworkService("https://localhost/function1"))
.isEqualTo(networkService);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/io/archiving/ArchiverTestUtils.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
/** Utilities for all {@link Archiver} unit tests. */
final class ArchiverTestUtils {
private ArchiverTestUtils() {}
/** Returns a byte array of length size that has values 0 .. size - 1. */
static byte[] newPreFilledByteArray(int size) {
return newPreFilledByteArray(0, size);
}
/** Returns a byte array of length size that has values offset .. offset + size - 1. */
static byte[] newPreFilledByteArray(int offset, int size) {
byte[] array = new byte[size];
for (int i = 0; i < size; i++) {
array[i] = (byte) (offset + i);
}
return array;
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/io/archiving/GoogleCloudStorageArchiverTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import static com.google.common.truth.Truth.assertThat;
import static com.google.tsunami.common.io.archiving.ArchiverTestUtils.newPreFilledByteArray;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.cloud.WriteChannel;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.inject.Inject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link GoogleCloudStorageArchiver}. */
@RunWith(JUnit4.class)
public final class GoogleCloudStorageArchiverTest {
private static final String BUCKET_ID = "test_bucket";
private static final String OBJECT_ID = "test/object";
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock Storage mockStorage;
@Mock WriteChannel mockWriter;
@Captor ArgumentCaptor blobInfoCaptor;
@Captor ArgumentCaptor byteDataCaptor;
@Captor ArgumentCaptor byteBufferCaptor;
@Inject private GoogleCloudStorageArchiver.Options options;
@Inject private GoogleCloudStorageArchiver.Factory archiverFactory;
@Before
public void setUp() {
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
bind(GoogleCloudStorageArchiver.Options.class)
.toInstance(new GoogleCloudStorageArchiver.Options());
install(new GoogleCloudStorageArchiverModule());
}
})
.injectMembers(this);
}
@Test
public void archive_withSmallSizeString_createsBlobInOneRequest() {
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
String dataToArchive = "TEST DATA";
boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive);
assertThat(succeeded).isTrue();
verify(mockStorage, times(1)).create(blobInfoCaptor.capture(), byteDataCaptor.capture());
assertThat(blobInfoCaptor.getValue())
.isEqualTo(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build());
assertThat(byteDataCaptor.getValue()).isEqualTo(dataToArchive.getBytes(UTF_8));
}
@Test
public void archive_withSmallSizeBlob_createsBlobInOneRequest() {
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
byte[] dataToArchive = newPreFilledByteArray(10);
boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive);
assertThat(succeeded).isTrue();
verify(mockStorage, times(1)).create(blobInfoCaptor.capture(), byteDataCaptor.capture());
assertThat(blobInfoCaptor.getValue())
.isEqualTo(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build());
assertThat(byteDataCaptor.getValue()).isEqualTo(dataToArchive);
}
@Test
public void archive_withLargeSizeString_createsBlobWithWriter() throws IOException {
options.chunkSizeInBytes = 8;
options.chunkUploadThresholdInBytes = 16;
doReturn(mockWriter)
.when(mockStorage)
.writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build()));
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
String dataToArchive = "THIS IS A LONG DATA";
int numOfChunks = (int) Math.ceil((double) dataToArchive.length() / options.chunkSizeInBytes);
boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive);
assertThat(succeeded).isTrue();
verify(mockWriter, times(numOfChunks)).write(byteBufferCaptor.capture());
assertThat(byteBufferCaptor.getAllValues())
.containsExactly(
ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 0, 8),
ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 8, 8),
ByteBuffer.wrap(dataToArchive.getBytes(UTF_8), 16, 3));
}
@Test
public void archive_withLargeSizeBlob_createsBlobWithWriter() throws IOException {
options.chunkSizeInBytes = 8;
options.chunkUploadThresholdInBytes = 16;
doReturn(mockWriter)
.when(mockStorage)
.writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build()));
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
byte[] dataToArchive = newPreFilledByteArray(20);
int numOfChunks = (int) Math.ceil((double) dataToArchive.length / options.chunkSizeInBytes);
boolean succeeded = archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive);
assertThat(succeeded).isTrue();
verify(mockWriter, times(numOfChunks)).write(byteBufferCaptor.capture());
assertThat(byteBufferCaptor.getAllValues())
.containsExactly(
ByteBuffer.wrap(dataToArchive, 0, 8),
ByteBuffer.wrap(dataToArchive, 8, 8),
ByteBuffer.wrap(dataToArchive, 16, 4));
}
@Test
public void archive_withLargeSizeBlobAndWriteError_returnsFalse() throws IOException {
options.chunkSizeInBytes = 8;
options.chunkUploadThresholdInBytes = 16;
doReturn(mockWriter)
.when(mockStorage)
.writer(eq(BlobInfo.newBuilder(BUCKET_ID, OBJECT_ID).build()));
doThrow(IOException.class).when(mockWriter).write(any());
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
byte[] dataToArchive = newPreFilledByteArray(20);
assertThat(archiver.archive(buildGcsUrl(BUCKET_ID, OBJECT_ID), dataToArchive)).isFalse();
}
@Test
public void archive_withInvalidGcsUrl_throwsIllegalArgumentException() {
GoogleCloudStorageArchiver archiver = archiverFactory.create(mockStorage);
assertThrows(IllegalArgumentException.class, () -> archiver.archive("invalid_url", ""));
}
private static final String buildGcsUrl(String bucketId, String objectId) {
return String.format("gs://%s/%s", bucketId, objectId);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/io/archiving/RawFileArchiverTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.io.archiving;
import static com.google.common.truth.Truth.assertThat;
import static com.google.tsunami.common.io.archiving.ArchiverTestUtils.newPreFilledByteArray;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link RawFileArchiver}. */
@RunWith(JUnit4.class)
public final class RawFileArchiverTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void archive_whenValidTargetFileAndByteArrayData_archivesGivenDataWithGivenName()
throws IOException {
File tempFile = temporaryFolder.newFile();
byte[] data = newPreFilledByteArray(200);
RawFileArchiver rawFileArchiver = new RawFileArchiver();
assertThat(rawFileArchiver.archive(tempFile.getAbsolutePath(), data)).isTrue();
assertThat(Files.toByteArray(tempFile)).isEqualTo(data);
}
@Test
public void archive_whenInvalidTargetFileAndByteArrayData_returnsFalse() throws IOException {
File tempFile = temporaryFolder.newFile();
byte[] data = newPreFilledByteArray(200);
RawFileArchiver rawFileArchiver = new RawFileArchiver();
assertThat(rawFileArchiver.archive(tempFile.getParent(), data)).isFalse();
assertThat(tempFile.length()).isEqualTo(0);
}
@Test
public void archive_whenValidTargetFileAndCharSequenceData_archivesGivenDataWithGivenName()
throws IOException {
File tempFile = temporaryFolder.newFile();
String data = "file data";
RawFileArchiver rawFileArchiver = new RawFileArchiver();
assertThat(rawFileArchiver.archive(tempFile.getAbsolutePath(), data)).isTrue();
assertThat(Files.asCharSource(tempFile, UTF_8).read()).isEqualTo(data);
}
@Test
public void archive_whenInvalidTargetFileAndCharSequenceData_returnsFalse() throws IOException {
File tempFile = temporaryFolder.newFile();
String data = "file data";
RawFileArchiver rawFileArchiver = new RawFileArchiver();
assertThat(rawFileArchiver.archive(tempFile.getParent(), data)).isFalse();
assertThat(tempFile.length()).isEqualTo(0);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/FuzzingUtilsTest.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.tsunami.common.net.http.HttpRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class FuzzingUtilsTest {
private static final HttpRequest REQUEST_WITHOUT_GET_PARAMETERS =
HttpRequest.get("https://google.com").withEmptyHeaders().build();
private static final HttpRequest REQUEST_WITH_GET_PARAMETERS =
HttpRequest.get("https://google.com?key=value&other=test").withEmptyHeaders().build();
@Test
public void fuzzGetParametersWithDefaultParameter_whenNoGetParameters_addsDefaultParameter() {
HttpRequest requestWithDefaultParameter =
HttpRequest.get("https://google.com?default=").withEmptyHeaders().build();
assertThat(
FuzzingUtils.fuzzGetParametersWithDefaultParameter(
REQUEST_WITHOUT_GET_PARAMETERS, "", "default"))
.contains(requestWithDefaultParameter);
}
@Test
public void fuzzGetParametersWithDefaultParameter_whenGetParameters_doesNotAddDefaultParameter() {
HttpRequest requestWithDefaultParameter =
HttpRequest.get("https://google.com?default=").withEmptyHeaders().build();
assertThat(
FuzzingUtils.fuzzGetParametersWithDefaultParameter(
REQUEST_WITH_GET_PARAMETERS, "", "default"))
.doesNotContain(requestWithDefaultParameter);
}
@Test
public void fuzzGetParametersWithDefaultParameter_whenGetParameters_fuzzesAllParameters() {
ImmutableList requestsWithFuzzedGetParameters =
ImmutableList.of(
HttpRequest.get("https://google.com?key=&other=test")
.withEmptyHeaders()
.build(),
HttpRequest.get("https://google.com?key=value&other=")
.withEmptyHeaders()
.build());
assertThat(
FuzzingUtils.fuzzGetParametersWithDefaultParameter(
REQUEST_WITH_GET_PARAMETERS, "", "default"))
.containsAtLeastElementsIn(requestsWithFuzzedGetParameters);
}
@Test
public void
fuzzGetParametersExpectingPathValues_whenGetParameterValueHasFileExtension_appendsFileExtensionToPayload() {
HttpRequest requestWithFileExtension =
HttpRequest.get("https://google.com?key=value.jpg").withEmptyHeaders().build();
HttpRequest requestWithFuzzedGetParameterWithFileExtension =
HttpRequest.get("https://google.com?key=%00.jpg").withEmptyHeaders().build();
assertThat(
FuzzingUtils.fuzzGetParametersExpectingPathValues(
requestWithFileExtension, ""))
.contains(requestWithFuzzedGetParameterWithFileExtension);
}
@Test
public void
fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefix_prefixesPayload() {
HttpRequest requestWithPathPrefix =
HttpRequest.get("https://google.com?key=resources/value").withEmptyHeaders().build();
HttpRequest requestWithFuzzedGetParameterWithPathPrefix =
HttpRequest.get("https://google.com?key=resources/").withEmptyHeaders().build();
assertThat(
FuzzingUtils.fuzzGetParametersExpectingPathValues(requestWithPathPrefix, ""))
.contains(requestWithFuzzedGetParameterWithPathPrefix);
}
@Test
public void
fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefixAndFileExtension_prefixesPayloadAndAppendsFileExtension() {
HttpRequest requestWithPathPrefixAndFileExtension =
HttpRequest.get("https://google.com?key=resources/value.jpg").withEmptyHeaders().build();
HttpRequest requestWithFuzzedGetParameterWithPathPrefixAndFileExtension =
HttpRequest.get("https://google.com?key=resources/%00.jpg")
.withEmptyHeaders()
.build();
assertThat(
FuzzingUtils.fuzzGetParametersExpectingPathValues(
requestWithPathPrefixAndFileExtension, ""))
.contains(requestWithFuzzedGetParameterWithPathPrefixAndFileExtension);
}
@Test
public void
fuzzGetParametersExpectingPathValues_whenGetParameterValueHasPathPrefixOrFileExtension_prefixesPayloadOrAppendsFileExtension() {
HttpRequest requestWithPathPrefixOrFileExtension =
HttpRequest.get("https://google.com?key=resources./value").withEmptyHeaders().build();
HttpRequest requestWithFuzzedGetParameterWithPathPrefixAndFileExtension =
HttpRequest.get("https://google.com?key=resources./%00./value")
.withEmptyHeaders()
.build();
assertThat(
FuzzingUtils.fuzzGetParametersExpectingPathValues(
requestWithPathPrefixOrFileExtension, ""))
.doesNotContain(requestWithFuzzedGetParameterWithPathPrefixAndFileExtension);
}
@Test
public void fuzzGetParameters_whenNoGetParameters_returnsEmptyList() {
assertThat(FuzzingUtils.fuzzGetParameters(REQUEST_WITHOUT_GET_PARAMETERS, ""))
.isEmpty();
}
@Test
public void fuzzGetParameters_whenGetParameters_fuzzesAllParameters() {
ImmutableList requestsWithFuzzedGetParameters =
ImmutableList.of(
HttpRequest.get("https://google.com?key=&other=test")
.withEmptyHeaders()
.build(),
HttpRequest.get("https://google.com?key=value&other=")
.withEmptyHeaders()
.build());
assertThat(FuzzingUtils.fuzzGetParameters(REQUEST_WITH_GET_PARAMETERS, ""))
.containsAtLeastElementsIn(requestsWithFuzzedGetParameters);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/UrlUtilsTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net;
import static com.google.common.truth.Truth.assertThat;
import static com.google.tsunami.common.net.UrlUtils.allSubPaths;
import static com.google.tsunami.common.net.UrlUtils.removeLeadingSlashes;
import static com.google.tsunami.common.net.UrlUtils.removeTrailingSlashes;
import static com.google.tsunami.common.net.UrlUtils.urlEncode;
import okhttp3.HttpUrl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link UrlUtils}. */
@RunWith(JUnit4.class)
public final class UrlUtilsTest {
@Test
public void allSubPaths_whenInvalidUrl_returnsEmptyList() {
assertThat(allSubPaths("invalid_url")).isEmpty();
}
@Test
public void allSubPaths_whenNoSubPathNoTrailingSlash_returnsSingleUrl() {
assertThat(allSubPaths("http://localhost")).containsExactly(HttpUrl.parse("http://localhost/"));
}
@Test
public void allSubPaths_whenNoSubPathWithTrailingSlash_returnsSingleUrl() {
assertThat(allSubPaths("http://localhost/"))
.containsExactly(HttpUrl.parse("http://localhost/"));
}
@Test
public void allSubPaths_whenValidQueryParamsAndFragments_removesParamsAndFragments() {
assertThat(allSubPaths("http://localhost/?param=value¶m2=value2#abc"))
.containsExactly(HttpUrl.parse("http://localhost/"));
}
@Test
public void allSubPaths_whenSingleSubPathsNoTrailingSlash_returnsExpectedUrl() {
assertThat(allSubPaths("http://localhost/a"))
.containsExactly(HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/"));
}
@Test
public void allSubPaths_whenSingleSubPathsWithTrailingSlash_returnsExpectedUrl() {
assertThat(allSubPaths("http://localhost/a/"))
.containsExactly(HttpUrl.parse("http://localhost/"), HttpUrl.parse("http://localhost/a/"));
}
@Test
public void allSubPaths_whenMultipleSubPathsNoTrailingSlash_returnsExpectedUrl() {
assertThat(allSubPaths("http://localhost/a/b/c"))
.containsExactly(
HttpUrl.parse("http://localhost/"),
HttpUrl.parse("http://localhost/a/"),
HttpUrl.parse("http://localhost/a/b/"),
HttpUrl.parse("http://localhost/a/b/c/"));
}
@Test
public void allSubPaths_whenMultipleSubPathsWithTrailingSlash_returnsExpectedUrl() {
assertThat(allSubPaths("http://localhost/a/b/c/"))
.containsExactly(
HttpUrl.parse("http://localhost/"),
HttpUrl.parse("http://localhost/a/"),
HttpUrl.parse("http://localhost/a/b/"),
HttpUrl.parse("http://localhost/a/b/c/"));
}
@Test
public void allSubPaths_whenMultipleSubPathsWithParamsAndFragments_returnsExpectedUrl() {
assertThat(allSubPaths("http://localhost/a/b/c/?param=value¶m2=value2#abc"))
.containsExactly(
HttpUrl.parse("http://localhost/"),
HttpUrl.parse("http://localhost/a/"),
HttpUrl.parse("http://localhost/a/b/"),
HttpUrl.parse("http://localhost/a/b/c/"));
}
@Test
public void removeLeadingSlashes_whenNoLeadingSlashes_returnsOriginal() {
assertThat(removeLeadingSlashes("a/b/c/")).isEqualTo("a/b/c/");
}
@Test
public void removeLeadingSlashes_whenSingleLeadingSlash_removesLeadingSlashes() {
assertThat(removeLeadingSlashes("/a/b/c/")).isEqualTo("a/b/c/");
}
@Test
public void removeLeadingSlashes_whenMultipleLeadingSlashes_removesLeadingSlashes() {
assertThat(removeLeadingSlashes("/////a/b/c/")).isEqualTo("a/b/c/");
}
@Test
public void removeTrailingSlashes_whenNoTrailingSlashes_returnsOriginal() {
assertThat(removeTrailingSlashes("/a/b/c")).isEqualTo("/a/b/c");
}
@Test
public void removeTrailingSlashes_whenSingleTrailingSlash_removesTrailingSlashes() {
assertThat(removeTrailingSlashes("/a/b/c/")).isEqualTo("/a/b/c");
}
@Test
public void removeTrailingSlashes_whenMultipleTrailingSlashes_removesTrailingSlashes() {
assertThat(removeTrailingSlashes("/a/b/c/////")).isEqualTo("/a/b/c");
}
@Test
public void urlEncode_whenEmptyString_returnsOriginal() {
assertThat(urlEncode("")).hasValue("");
}
@Test
public void urlEncode_whenNothingToEncode_returnsOriginal() {
assertThat(urlEncode("abcdefghijklmnopqrstuvwxyz")).hasValue("abcdefghijklmnopqrstuvwxyz");
assertThat(urlEncode("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).hasValue("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
assertThat(urlEncode("0123456789")).hasValue("0123456789");
assertThat(urlEncode("-_.*")).hasValue("-_.*");
}
@Test
public void urlEncode_whenNotEncoded_returnsEncoded() {
assertThat(urlEncode(" ")).hasValue("+");
assertThat(urlEncode("()[]{}<>")).hasValue("%28%29%5B%5D%7B%7D%3C%3E");
assertThat(urlEncode("?!@#$%^&=+,;:'\"`/\\|~"))
.hasValue("%3F%21%40%23%24%25%5E%26%3D%2B%2C%3B%3A%27%22%60%2F%5C%7C%7E");
}
@Test
public void urlEncode_whenAlreadyEncoded_encodesAgain() {
assertThat(urlEncode("%2F")).hasValue("%252F");
assertThat(urlEncode("%252F")).hasValue("%25252F");
}
@Test
public void urlEncode_whenComplexEncoding_encodesCorrectly() {
assertThat(urlEncode("£")).hasValue("%C2%A3");
assertThat(urlEncode("つ")).hasValue("%E3%81%A4");
assertThat(urlEncode("äëïöüÿ")).hasValue("%C3%A4%C3%AB%C3%AF%C3%B6%C3%BC%C3%BF");
assertThat(urlEncode("ÄËÏÖÜŸ")).hasValue("%C3%84%C3%8B%C3%8F%C3%96%C3%9C%C5%B8");
}
@Test
public void urlEncode_whenUnicode_encodesOriginal() {
// EURO sign
assertThat(urlEncode("\u20AC")).hasValue("%E2%82%AC");
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpClientModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.truth.Truth.assertThat;
import static com.google.tsunami.common.net.http.HttpRequest.get;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.tsunami.common.net.http.HttpClientModule.ConnectTimeout;
import com.google.tsunami.common.net.http.HttpClientModule.FollowRedirects;
import com.google.tsunami.common.net.http.HttpClientModule.MaxRequests;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.time.Duration;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link HttpClientModule}. */
@RunWith(JUnit4.class)
public final class HttpClientModuleTest {
private static final String TESTING_KEYSTORE = "testdata/tsunami_test_server.p12";
private static final char[] TESTING_KEYSTORE_PASSWORD = "tsunamitest".toCharArray();
private final HttpClientCliOptions cliOptions = new HttpClientCliOptions();
private final HttpClientConfigProperties configProperties = new HttpClientConfigProperties();
@Test
public void provideHttpClient_always_createsSingleton() {
Injector injector =
Guice.createInjector(new HttpClientModule.Builder().setMaxRequests(10).build());
HttpClient httpClient = injector.getInstance(HttpClient.class);
HttpClient httpClient2 = injector.getInstance(HttpClient.class);
assertThat(httpClient).isSameInstanceAs(httpClient2);
}
@Test
public void setConnectionPoolMaxIdle_whenNonPositiveMaxIdle_throwsIllegalArgumentException() {
HttpClientModule.Builder builder = new HttpClientModule.Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setConnectionPoolMaxIdle(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setConnectionPoolMaxIdle(0));
}
@Test
public void
setConnectionPoolKeepAliveDuration_whenNegativeDuration_throwsIllegalArgumentException() {
HttpClientModule.Builder builder = new HttpClientModule.Builder();
assertThrows(
IllegalArgumentException.class,
() -> builder.setConnectionPoolKeepAliveDuration(Duration.ofMillis(-1)));
}
@Test
public void setMaxRequests_whenPositiveRequests_setsValueToDispatcher() {
Injector injector =
Guice.createInjector(new HttpClientModule.Builder().setMaxRequests(10).build());
assertThat(injector.getInstance(Key.get(int.class, MaxRequests.class))).isEqualTo(10);
}
@Test
public void setMaxRequests_whenNonPositiveRequests_throwsIllegalArgumentException() {
HttpClientModule.Builder builder = new HttpClientModule.Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setMaxRequests(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setMaxRequests(0));
}
@Test
public void setFollowRedirects_always_setsValueToClient() {
Injector injector =
Guice.createInjector(new HttpClientModule.Builder().setFollowRedirects(true).build());
assertTrue(injector.getInstance(Key.get(Boolean.class, FollowRedirects.class)));
}
@Test
public void setTrustAllCertificates_whenFalseAndCertIsInvalid_throws()
throws GeneralSecurityException, IOException {
cliOptions.trustAllCertificates = false;
configProperties.trustAllCertificates = false;
HttpClient httpClient =
Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class);
MockWebServer mockWebServer = startMockWebServerWithSsl();
// The certificate used in test is a self-signed one. HttpClient will reject it unless the
// certificate is explicitly trusted.
assertThrows(
SSLHandshakeException.class,
() -> httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build()));
// Note: b/314642696 - After this point, the socket in mockWebServer was closed when the
// exception was raised. So working with the mockWebServer would be
// hazardous.
}
@Test
public void setTrustAllCertificates_whenBothCliAndConfigValuesAreSet_cliValueTakesPrecedence()
throws GeneralSecurityException, IOException {
cliOptions.trustAllCertificates = false;
configProperties.trustAllCertificates = true;
HttpClient httpClient =
Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class);
MockWebServer mockWebServer = startMockWebServerWithSsl();
// The certificate used in test is a self-signed one. HttpClient will reject it unless the
// certificate is explicitly trusted.
assertThrows(
SSLHandshakeException.class,
() -> httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build()));
// Note: b/314642696 - After this point, the socket in mockWebServer was closed when the
// exception was raised. So working with the mockWebServer would be
// hazardous.
}
@Test
public void setTrustAllCertificates_whenCliOptionEnabledAndCertIsInvalid_ignoresCertError()
throws GeneralSecurityException, IOException {
cliOptions.trustAllCertificates = true;
HttpClient httpClient =
Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class);
MockWebServer mockWebServer = startMockWebServerWithSsl();
HttpResponse response = httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build());
assertThat(response.bodyString()).hasValue("body");
mockWebServer.shutdown();
}
@Test
public void
setTrustAllCertificates_whenConfigPropropertyEnabledAndCertIsInvalid_ignoresCertError()
throws GeneralSecurityException, IOException {
configProperties.trustAllCertificates = true;
HttpClient httpClient =
Guice.createInjector(getTestingGuiceModuleWithConfigs()).getInstance(HttpClient.class);
MockWebServer mockWebServer = startMockWebServerWithSsl();
HttpResponse response = httpClient.send(get(mockWebServer.url("/")).withEmptyHeaders().build());
assertThat(response.bodyString()).hasValue("body");
mockWebServer.shutdown();
}
@Test
public void setConnectTimeoutSeconds_whenSpecifiedUsingCliOptions_setsValueFromCli() {
cliOptions.connectTimeoutSeconds = 50;
Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs());
assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class)))
.isEqualTo(Duration.ofSeconds(50));
}
@Test
public void setConnectTimeoutSeconds_whenSpecifiedUsingConfigProperties_setsValueFromConfig() {
configProperties.connectTimeoutSeconds = 50;
Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs());
assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class)))
.isEqualTo(Duration.ofSeconds(50));
}
@Test
public void setConnectTimeoutSeconds_whenBothCliAndConfigAreSet_cliTakesPrecedence() {
cliOptions.connectTimeoutSeconds = 50;
configProperties.connectTimeoutSeconds = 30;
Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs());
assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class)))
.isEqualTo(Duration.ofSeconds(50));
}
@Test
public void setConnectTimeoutSeconds_whenBothCliAndConfigAreNotSet_setsDefaultValue() {
Injector injector = Guice.createInjector(getTestingGuiceModuleWithConfigs());
assertThat(injector.getInstance(Key.get(Duration.class, ConnectTimeout.class)))
.isEqualTo(Duration.ofSeconds(10));
}
private AbstractModule getTestingGuiceModuleWithConfigs() {
return new AbstractModule() {
@Override
protected void configure() {
install(new HttpClientModule.Builder().build());
bind(HttpClientCliOptions.class).toInstance(cliOptions);
bind(HttpClientConfigProperties.class).toInstance(configProperties);
}
};
}
private MockWebServer startMockWebServerWithSsl() throws GeneralSecurityException, IOException {
MockWebServer mockWebServer = new MockWebServer();
mockWebServer.useHttps(getTestingSslSocketFactory(), false);
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("body"));
mockWebServer.start();
return mockWebServer;
}
private SSLSocketFactory getTestingSslSocketFactory()
throws GeneralSecurityException, IOException {
final KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(getClass().getResourceAsStream(TESTING_KEYSTORE), TESTING_KEYSTORE_PASSWORD);
keyManagerFactory.init(keyStore, TESTING_KEYSTORE_PASSWORD);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
return sslContext.getSocketFactory();
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpHeadersTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableListMultimap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link HttpHeaders}. */
@RunWith(JUnit4.class)
public class HttpHeadersTest {
@Test
public void builderAddHeader_always_putsInHeadersMap() {
HttpHeaders httpHeaders = HttpHeaders.builder().addHeader("test_header", "test_value").build();
assertThat(httpHeaders.rawHeaders())
.containsExactlyEntriesIn(ImmutableListMultimap.of("test_header", "test_value"));
}
@Test
public void builderAddHeader_withKnownHeader_canonicalizesHeaderName() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT.toUpperCase(), "test_value")
.build();
assertThat(httpHeaders.rawHeaders())
.containsExactlyEntriesIn(
ImmutableListMultimap.of(com.google.common.net.HttpHeaders.ACCEPT, "test_value"));
}
@Test
public void builderAddHeader_whenEnableCanonicalization_canonicalizesHeaderName() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader("TEST_Header", "test_value", true)
.build();
assertThat(httpHeaders.rawHeaders())
.containsExactlyEntriesIn(
ImmutableListMultimap.of("test_header", "test_value"));
}
@Test
public void builderAddHeader_whenDisableCanonicalization_addsHeaderNameAsIs() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader("TEST_Header", "test_value", false)
.build();
assertThat(httpHeaders.rawHeaders())
.containsExactlyEntriesIn(
ImmutableListMultimap.of("TEST_Header", "test_value"));
}
@Test
public void builderAddHeader_withNullName_throwsNullPointerException() {
assertThrows(
NullPointerException.class, () -> HttpHeaders.builder().addHeader(null, "test_value"));
}
@Test
public void builderAddHeader_withNullValue_throwsNullPointerException() {
assertThrows(
NullPointerException.class, () -> HttpHeaders.builder().addHeader("test_header", null));
}
@Test
public void builderAddHeader_withIllegalHeaderName_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> HttpHeaders.builder().addHeader(":::", "test_value"));
}
@Test
public void builderAddHeader_withIllegalHeaderValue_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> HttpHeaders.builder().addHeader("test_header", String.valueOf((char) 11)));
}
@Test
public void names_always_returnsAllHeaderNames() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.names())
.containsExactly(
com.google.common.net.HttpHeaders.ACCEPT,
com.google.common.net.HttpHeaders.CONTENT_TYPE);
}
@Test
public void get_whenRequestedHeaderExists_returnsRequestedHeader() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.build();
assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.ACCEPT)).hasValue("*/*");
}
@Test
public void get_whenMultipleValuesExist_returnsFirstValue() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.ACCEPT)).hasValue("*/*");
}
@Test
public void get_whenRequestedHeaderDoesNotExist_returnsEmpty() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.get(com.google.common.net.HttpHeaders.COOKIE)).isEmpty();
}
@Test
public void get_withNullHeaderName_throwsNullPointerException() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThrows(NullPointerException.class, () -> httpHeaders.get(null));
}
@Test
public void getAll_always_returnsAllRequestedValues() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.ACCEPT))
.containsExactly("*/*", "text/html");
}
@Test
public void getAll_withKnownHeaderValue_canonicalizesRequestedHeader() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.ACCEPT.toUpperCase()))
.containsExactly("*/*", "text/html");
}
@Test
public void getAll_whenRequestValueDoesNotExist_returnsEmptyList() {
HttpHeaders httpHeaders =
HttpHeaders.builder()
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "*/*")
.addHeader(com.google.common.net.HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
.addHeader(com.google.common.net.HttpHeaders.ACCEPT, "text/html")
.build();
assertThat(httpHeaders.getAll(com.google.common.net.HttpHeaders.COOKIE)).isEmpty();
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpRequestTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.protobuf.ByteString;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link HttpRequest}. */
@RunWith(JUnit4.class)
public class HttpRequestTest {
@Test
public void get_always_buildsHttpGetRequest() {
HttpRequest httpRequest = HttpRequest.get("http://localhost/url").withEmptyHeaders().build();
assertThat(httpRequest.method()).isEqualTo(HttpMethod.GET);
assertThat(httpRequest.url()).isEqualTo("http://localhost/url");
}
@Test
public void head_always_buildsHttpHeadRequest() {
HttpRequest httpRequest = HttpRequest.head("http://localhost/url").withEmptyHeaders().build();
assertThat(httpRequest.method()).isEqualTo(HttpMethod.HEAD);
assertThat(httpRequest.url()).isEqualTo("http://localhost/url");
}
@Test
public void post_always_buildsHttpPostRequest() {
HttpRequest httpRequest = HttpRequest.post("http://localhost/url").withEmptyHeaders().build();
assertThat(httpRequest.method()).isEqualTo(HttpMethod.POST);
assertThat(httpRequest.url()).isEqualTo("http://localhost/url");
}
@Test
public void delete_always_buildsHttpDeleteRequest() {
HttpRequest httpRequest = HttpRequest.delete("http://localhost/url").withEmptyHeaders().build();
assertThat(httpRequest.method()).isEqualTo(HttpMethod.DELETE);
assertThat(httpRequest.url()).isEqualTo("http://localhost/url");
}
@Test
public void build_whenGetRequestHasRequestBody_throwsIllegalStateException() {
assertThrows(
IllegalStateException.class,
() ->
HttpRequest.builder()
.setMethod(HttpMethod.GET)
.setUrl("http://localhost")
.setHeaders(HttpHeaders.builder().build())
.setRequestBody(ByteString.EMPTY)
.build());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/http/HttpResponseTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.protobuf.ByteString;
import okhttp3.HttpUrl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link HttpResponse}. */
@RunWith(JUnit4.class)
public final class HttpResponseTest {
private static final HttpUrl TEST_URL = HttpUrl.parse("https://example.com/");
@Test
public void bodyJson_whenValidResponseBody_returnsParsedJson() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8("{ \"test_value\": 1 }"))
.setResponseUrl(TEST_URL)
.build();
assertThat(httpResponse.bodyJson()).isPresent();
assertThat(httpResponse.bodyJson().get().isJsonObject()).isTrue();
assertThat(
httpResponse
.bodyJson()
.get()
.getAsJsonObject()
.getAsJsonPrimitive("test_value")
.getAsInt())
.isEqualTo(1);
}
@Test
public void bodyJson_whenEmptyResponseBody_returnsEmptyOptional() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setResponseUrl(TEST_URL)
.build();
assertThat(httpResponse.bodyJson()).isEmpty();
}
@Test
public void bodyJson_whenNonJsonResponseBody_returnsEmptyOptional() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8("not a json"))
.setResponseUrl(TEST_URL)
.build();
assertThat(httpResponse.bodyJson()).isEmpty();
}
@Test
public void bodyJson_whenEmptyBodyResponseBody_throwsJsonSyntaxException() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8(""))
.setResponseUrl(TEST_URL)
.build();
assertThrows(
IllegalStateException.class, () -> httpResponse.jsonFieldEqualsToValue("field", "value"));
}
@Test
public void jsonFieldEqualsToValue_whenEmptyJsonResponseBody_returnsFalse() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8("{}"))
.setResponseUrl(TEST_URL)
.build();
assertFalse(httpResponse.jsonFieldEqualsToValue("field", "value"));
}
@Test
public void jsonFieldEqualsToValue_whenNonJsonResponseBody_returnsEmptyOptional() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8("not a json"))
.setResponseUrl(TEST_URL)
.build();
assertThat(httpResponse.bodyJson()).isEmpty();
}
@Test
public void jsonFieldEqualsToValue_whenJsonFieldContainsValue_returnsTrue() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(HttpHeaders.builder().build())
.setBodyBytes(ByteString.copyFromUtf8("{\"field\": \"value\"}"))
.setResponseUrl(TEST_URL)
.build();
assertTrue(httpResponse.jsonFieldEqualsToValue("field", "value"));
}
@Test
public void bodyJson_whenHttpStatusInvalid_parseSucceeds() {
HttpResponse httpResponse =
HttpResponse.builder()
.setStatus(HttpStatus.HTTP_STATUS_UNSPECIFIED)
.setHeaders(HttpHeaders.builder().build())
.setResponseUrl(TEST_URL)
.build();
assertFalse(httpResponse.status().isSuccess());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/http/OkHttpHttpClientTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.http;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.HOST;
import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static com.google.common.truth.Truth.assertThat;
import static com.google.tsunami.common.net.http.HttpRequest.get;
import static com.google.tsunami.common.net.http.HttpRequest.head;
import static com.google.tsunami.common.net.http.HttpRequest.post;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import com.google.common.net.MediaType;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.protobuf.ByteString;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link OkHttpHttpClient}. */
@RunWith(JUnit4.class)
public final class OkHttpHttpClientTest {
private static final String TESTING_KEYSTORE = "testdata/tsunami_test_server.p12";
private static final char[] TESTING_KEYSTORE_PASSWORD = "tsunamitest".toCharArray();
private MockWebServer mockWebServer;
@Inject private HttpClient httpClient;
@Before
public void setUp() {
mockWebServer = new MockWebServer();
Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this);
}
@After
public void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
public void sendAsIs_always_returnsExpectedHttpResponse()
throws IOException, InterruptedException {
mockWebServer.setDispatcher(new SendAsIsTestDispatcher());
mockWebServer.start();
String expectedResponseBody = SendAsIsTestDispatcher.buildBody("GET", "");
HttpUrl baseUrl = mockWebServer.url("/");
String requestUrl =
new URL(
baseUrl.scheme(),
baseUrl.host(),
baseUrl.port(),
"/send-as-is/%2e%2e/%2e%2e/etc/passwd")
.toString();
HttpResponse response = httpClient.sendAsIs(get(requestUrl).withEmptyHeaders().build());
assertThat(mockWebServer.takeRequest().getPath())
.isEqualTo("/send-as-is/%2e%2e/%2e%2e/etc/passwd");
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(expectedResponseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(expectedResponseBody, UTF_8))
.build());
}
@Test
public void sendAsIs_withPostRequest_returnsExpectedHttpResponse()
throws IOException, InterruptedException {
mockWebServer.setDispatcher(new SendAsIsTestDispatcher());
mockWebServer.start();
String requestBody = "POST BODY";
String expectedResponseBody = SendAsIsTestDispatcher.buildBody("POST", requestBody);
HttpUrl baseUrl = mockWebServer.url("/");
String requestUrl =
new URL(baseUrl.scheme(), baseUrl.host(), baseUrl.port(), "/send-as-is/%2e%2e/%2e%2e/path")
.toString();
HttpResponse response =
httpClient.sendAsIs(
post(requestUrl)
.setRequestBody(ByteString.copyFrom(requestBody, UTF_8))
.withEmptyHeaders()
.build());
assertThat(mockWebServer.takeRequest().getPath()).isEqualTo("/send-as-is/%2e%2e/%2e%2e/path");
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(expectedResponseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(expectedResponseBody, UTF_8))
.build());
}
@Test
public void send_always_canonicalizesRequestUrl() throws IOException, InterruptedException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
HttpUrl baseUrl = mockWebServer.url("/");
String requestUrl =
new URL(baseUrl.scheme(), baseUrl.host(), baseUrl.port(), "/%2e%2e/%2e%2e/etc/passwd")
.toString();
httpClient.send(get(requestUrl).withEmptyHeaders().build());
assertThat(mockWebServer.takeRequest().getPath()).isEqualTo("/etc/passwd");
}
@Test
public void send_whenGetRequest_returnsExpectedHttpResponse() throws IOException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/get").toString();
HttpResponse response = httpClient.send(get(requestUrl).withEmptyHeaders().build());
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void sendAsync_whenGetRequest_returnsExpectedHttpResponse()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/get").toString();
HttpResponse response = httpClient.sendAsync(get(requestUrl).withEmptyHeaders().build()).get();
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void send_whenHeadRequest_returnsHttpResponseWithoutBody() throws IOException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/head").toString();
HttpResponse response = httpClient.send(head(requestUrl).withEmptyHeaders().build());
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(Optional.empty())
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void sendAsync_whenHeadRequest_returnsHttpResponseWithoutBody()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/head").toString();
HttpResponse response = httpClient.sendAsync(head(requestUrl).withEmptyHeaders().build()).get();
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(Optional.empty())
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void send_whenPostRequest_returnsExpectedHttpResponse() throws IOException {
String responseBody = "{ \"test\": \"json\" }";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/post").toString();
HttpResponse response =
httpClient.send(
post(requestUrl)
.setHeaders(
HttpHeaders.builder()
.addHeader(ACCEPT, MediaType.JSON_UTF_8.toString())
.build())
.build());
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void sendAsync_whenPostRequest_returnsExpectedHttpResponse()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "{ \"test\": \"json\" }";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/post").toString();
HttpResponse response =
httpClient
.sendAsync(
post(requestUrl)
.setHeaders(
HttpHeaders.builder()
.addHeader(ACCEPT, MediaType.JSON_UTF_8.toString())
.build())
.build())
.get();
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void send_whenPostRequestWithEmptyHeaders_returnsExpectedHttpResponse()
throws IOException {
String responseBody = "{ \"test\": \"json\" }";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/post").toString();
HttpResponse response = httpClient.send(post(requestUrl).withEmptyHeaders().build());
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void sendAsync_whenPostRequestWithEmptyHeaders_returnsExpectedHttpResponse()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "{ \"test\": \"json\" }";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
String requestUrl = mockWebServer.url("/test/post").toString();
HttpResponse response = httpClient.sendAsync(post(requestUrl).withEmptyHeaders().build()).get();
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
// MockWebServer always adds this response header.
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(HttpUrl.parse(requestUrl))
.build());
}
@Test
public void send_whenFollowRedirect_returnsFinalHttpResponse() throws IOException {
String responseBody = "test response";
mockWebServer.setDispatcher(new RedirectDispatcher(responseBody));
mockWebServer.start();
HttpResponse response =
httpClient
.modify()
.setFollowRedirects(true)
.build()
.send(
get(mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString())
.withEmptyHeaders()
.build());
HttpUrl redirectDestinationUrl =
HttpUrl.parse(mockWebServer.url(RedirectDispatcher.REDIRECT_DESTINATION_PATH).toString());
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(redirectDestinationUrl)
.build());
}
@Test
public void sendAsync_whenFollowRedirect_returnsFinalHttpResponse()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "test response";
mockWebServer.setDispatcher(new RedirectDispatcher(responseBody));
mockWebServer.start();
HttpUrl redirectDestinationUrl =
HttpUrl.parse(mockWebServer.url(RedirectDispatcher.REDIRECT_DESTINATION_PATH).toString());
HttpResponse response =
httpClient
.modify()
.setFollowRedirects(true)
.build()
.sendAsync(
get(mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString())
.withEmptyHeaders()
.build())
.get();
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.OK)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, String.valueOf(responseBody.length()))
.build())
.setBodyBytes(ByteString.copyFrom(responseBody, UTF_8))
.setResponseUrl(redirectDestinationUrl)
.build());
}
@Test
public void send_whenNotFollowRedirect_returnsFinalHttpResponse() throws IOException {
String responseBody = "test response";
mockWebServer.setDispatcher(new RedirectDispatcher(responseBody));
mockWebServer.start();
String redirectingUrl = mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString();
HttpResponse response =
httpClient
.modify()
.setFollowRedirects(false)
.build()
.send(get(redirectingUrl).withEmptyHeaders().build());
assertThat(response.status()).isEqualTo(HttpStatus.FOUND);
assertThat(response.headers())
.isEqualTo(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, "0")
.addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH)
.build());
assertThat(response.bodyString()).hasValue("");
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.FOUND)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, "0")
.addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH)
.build())
.setBodyBytes(ByteString.EMPTY)
.setResponseUrl(HttpUrl.parse(redirectingUrl))
.build());
}
@Test
public void sendAsync_whenNotFollowRedirect_returnsFinalHttpResponse()
throws IOException, ExecutionException, InterruptedException {
String responseBody = "test response";
mockWebServer.setDispatcher(new RedirectDispatcher(responseBody));
mockWebServer.start();
String redirectingUrl = mockWebServer.url(RedirectDispatcher.REDIRECT_PATH).toString();
HttpResponse response =
httpClient
.modify()
.setFollowRedirects(false)
.build()
.sendAsync(get(redirectingUrl).withEmptyHeaders().build())
.get();
assertThat(response.status()).isEqualTo(HttpStatus.FOUND);
assertThat(response.headers())
.isEqualTo(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, "0")
.addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH)
.build());
assertThat(response.bodyString()).hasValue("");
assertThat(response)
.isEqualTo(
HttpResponse.builder()
.setStatus(HttpStatus.FOUND)
.setHeaders(
HttpHeaders.builder()
.addHeader(CONTENT_LENGTH, "0")
.addHeader(LOCATION, RedirectDispatcher.REDIRECT_DESTINATION_PATH)
.build())
.setBodyBytes(ByteString.EMPTY)
.setResponseUrl(HttpUrl.parse(redirectingUrl))
.build());
}
@Test
public void send_whenNoUserAgentInRequest_setsCorrectUserAgentHeader() throws IOException {
mockWebServer.setDispatcher(new UserAgentTestDispatcher());
mockWebServer.start();
HttpResponse response =
httpClient.send(
get(mockWebServer.url(UserAgentTestDispatcher.USERAGENT_TEST_PATH).toString())
.withEmptyHeaders()
.build());
assertThat(response.status()).isEqualTo(HttpStatus.OK);
}
@Test
public void send_whenUserAgentSetInRequest_overridesUserAgentHeader() throws IOException {
mockWebServer.setDispatcher(new UserAgentTestDispatcher());
mockWebServer.start();
HttpResponse response =
httpClient.send(
get(mockWebServer.url(UserAgentTestDispatcher.USERAGENT_TEST_PATH).toString())
.setHeaders(
HttpHeaders.builder().addHeader(USER_AGENT, "User Agent In Request").build())
.build());
assertThat(response.status()).isEqualTo(HttpStatus.OK);
}
@Test
public void send_whenRequestFailed_throwsException() {
assertThrows(
IOException.class,
() -> httpClient.send(get("http://unknownhost/path").withEmptyHeaders().build()));
}
@Test
public void sendAsync_whenRequestFailed_returnsFutureWithException() {
ListenableFuture responseFuture =
httpClient.sendAsync(get("http://unknownhost/path").withEmptyHeaders().build());
ExecutionException ex = assertThrows(ExecutionException.class, responseFuture::get);
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
}
@Test
public void send_whenHostnameAndIpInRequest_useHostnameAsProxy() throws IOException {
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
String host = "host.com";
mockWebServer.setDispatcher(new HostnameTestDispatcher(host));
mockWebServer.start(loopbackAddress, 0);
int port = mockWebServer.url("/").port();
NetworkService networkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
NetworkEndpointUtils.forIpHostnameAndPort(
loopbackAddress.getHostAddress(), host, port))
.build();
// The request to host.com should be sent through mockWebServer's IP.
HttpResponse response =
httpClient.send(
get(String.format("http://host.com:%d/test/get", port)).withEmptyHeaders().build(),
networkService);
assertThat(response.status()).isEqualTo(HttpStatus.OK);
}
@Test
public void send_whenInvalidCertificatesAreIgnored_getResponseWithoutException()
throws GeneralSecurityException, IOException {
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
String host = "host.com";
MockWebServer mockWebServer = startMockWebServerWithSsl(loopbackAddress);
int port = mockWebServer.url("/").port();
NetworkService networkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
NetworkEndpointUtils.forIpHostnameAndPort(
loopbackAddress.getHostAddress(), host, port))
.build();
HttpClientCliOptions cliOptions = new HttpClientCliOptions();
HttpClientConfigProperties configProperties = new HttpClientConfigProperties();
cliOptions.trustAllCertificates = configProperties.trustAllCertificates = true;
HttpClient httpClient =
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
install(new HttpClientModule.Builder().build());
bind(HttpClientCliOptions.class).toInstance(cliOptions);
bind(HttpClientConfigProperties.class).toInstance(configProperties);
}
})
.getInstance(HttpClient.class);
HttpResponse response =
httpClient.send(
get(String.format("https://%s:%d", host, port)).withEmptyHeaders().build(),
networkService);
assertThat(response.bodyString()).hasValue("body");
mockWebServer.shutdown();
}
@Test
public void send_whenInvalidCertificatesAreNotIgnored_throws()
throws GeneralSecurityException, IOException {
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
String host = "host.com";
MockWebServer mockWebServer = startMockWebServerWithSsl(loopbackAddress);
int port = mockWebServer.url("/").port();
NetworkService networkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
NetworkEndpointUtils.forIpHostnameAndPort(
loopbackAddress.getHostAddress(), host, port))
.build();
HttpClientCliOptions cliOptions = new HttpClientCliOptions();
HttpClientConfigProperties configProperties = new HttpClientConfigProperties();
cliOptions.trustAllCertificates = configProperties.trustAllCertificates = false;
HttpClient httpClient =
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
install(new HttpClientModule.Builder().build());
bind(HttpClientCliOptions.class).toInstance(cliOptions);
bind(HttpClientConfigProperties.class).toInstance(configProperties);
}
})
.getInstance(HttpClient.class);
assertThrows(
SSLException.class,
() ->
httpClient.send(
get(String.format("https://%s:%d", host, port)).withEmptyHeaders().build(),
networkService));
mockWebServer.shutdown();
}
@Test
public void send_default_userAgent() throws IOException, InterruptedException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
HttpUrl baseUrl = mockWebServer.url("/");
httpClient.send(get(baseUrl.toString()).withEmptyHeaders().build());
assertThat(mockWebServer.takeRequest().getHeader(USER_AGENT))
.isEqualTo(HttpClient.TSUNAMI_USER_AGENT);
}
@Test
public void send_overridden_userAgent() throws IOException, InterruptedException {
String responseBody = "test response";
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(HttpStatus.OK.code())
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(responseBody));
mockWebServer.start();
final String userAgentOverride = "User Agent In Override";
HttpClientCliOptions cliOptions = new HttpClientCliOptions();
cliOptions.userAgent = userAgentOverride;
HttpClientConfigProperties configProperties = new HttpClientConfigProperties();
cliOptions.trustAllCertificates = configProperties.trustAllCertificates = true;
HttpClient httpClient =
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
install(new HttpClientModule.Builder().build());
bind(HttpClientCliOptions.class).toInstance(cliOptions);
bind(HttpClientConfigProperties.class).toInstance(configProperties);
}
})
.getInstance(HttpClient.class);
HttpUrl baseUrl = mockWebServer.url("/");
httpClient.send(get(baseUrl.toString()).withEmptyHeaders().build());
assertThat(mockWebServer.takeRequest().getHeader(USER_AGENT)).isEqualTo(userAgentOverride);
}
private MockWebServer startMockWebServerWithSsl(InetAddress serverAddress)
throws GeneralSecurityException, IOException {
MockWebServer mockWebServer = new MockWebServer();
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("body"));
mockWebServer.useHttps(getTestingSslSocketFactory(), false);
mockWebServer.start(serverAddress, 0);
return mockWebServer;
}
private SSLSocketFactory getTestingSslSocketFactory()
throws GeneralSecurityException, IOException {
final KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(getClass().getResourceAsStream(TESTING_KEYSTORE), TESTING_KEYSTORE_PASSWORD);
keyManagerFactory.init(keyStore, TESTING_KEYSTORE_PASSWORD);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
return sslContext.getSocketFactory();
}
static final class RedirectDispatcher extends Dispatcher {
static final String REDIRECT_PATH = "/redirect";
static final String REDIRECT_DESTINATION_PATH = "/redirect-dest";
private final String responseBody;
RedirectDispatcher(String responseBody) {
this.responseBody = checkNotNull(responseBody);
}
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
switch (recordedRequest.getPath()) {
case REDIRECT_PATH:
return new MockResponse()
.setResponseCode(HttpStatus.FOUND.code())
.setHeader(LOCATION, "/redirect-dest");
case REDIRECT_DESTINATION_PATH:
return new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(responseBody);
default:
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}
}
static final class UserAgentTestDispatcher extends Dispatcher {
static final String USERAGENT_TEST_PATH = "/useragent-test";
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
if (recordedRequest.getPath().equals(USERAGENT_TEST_PATH)
&& nullToEmpty(recordedRequest.getHeader(USER_AGENT)).equals("TsunamiSecurityScanner")) {
return new MockResponse().setResponseCode(HttpStatus.OK.code());
}
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}
static final class HostnameTestDispatcher extends Dispatcher {
private final String expectedHost;
HostnameTestDispatcher(String expectedHost) {
this.expectedHost = checkNotNull(expectedHost);
}
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
if (nullToEmpty(recordedRequest.getHeader(HOST)).startsWith(expectedHost)) {
return new MockResponse().setResponseCode(HttpStatus.OK.code());
}
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}
static final class SendAsIsTestDispatcher extends Dispatcher {
static final String SEND_AS_IS_PATH = "/send-as-is/";
static String buildBody(String method, String requestBody) {
return String.format("Method: %s\nRequest Body: %s", method, requestBody);
}
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
if (recordedRequest.getPath().startsWith(SEND_AS_IS_PATH)) {
return new MockResponse()
.setHeader(CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString())
.setBody(buildBody(recordedRequest.getMethod(), recordedRequest.getBody().readUtf8()))
.setResponseCode(HttpStatus.OK.code());
}
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/socket/DefaultTsunamiSocketFactoryTest.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.time.Duration;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link DefaultTsunamiSocketFactory}. */
@RunWith(JUnit4.class)
public final class DefaultTsunamiSocketFactoryTest {
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(30);
private SocketFactory mockSocketFactory;
private SSLSocketFactory mockSslSocketFactory;
private Socket mockSocket;
private SSLSocket mockSslSocket;
private DefaultTsunamiSocketFactory tsunamiSocketFactory;
@Before
public void setUp() throws IOException {
mockSocketFactory = mock(SocketFactory.class);
mockSslSocketFactory = mock(SSLSocketFactory.class);
mockSocket = mock(Socket.class);
mockSslSocket = mock(SSLSocket.class);
when(mockSocketFactory.createSocket()).thenReturn(mockSocket);
when(mockSslSocketFactory.createSocket(any(Socket.class), anyString(), anyInt(), anyBoolean()))
.thenReturn(mockSslSocket);
tsunamiSocketFactory =
new DefaultTsunamiSocketFactory(
mockSocketFactory, mockSslSocketFactory, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
}
@Test
public void constructor_withNullSocketFactory_throwsException() {
assertThrows(
NullPointerException.class,
() ->
new DefaultTsunamiSocketFactory(
null, mockSslSocketFactory, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT));
}
@Test
public void constructor_withNullSslSocketFactory_throwsException() {
assertThrows(
NullPointerException.class,
() ->
new DefaultTsunamiSocketFactory(
mockSocketFactory, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT));
}
@Test
public void constructor_withNegativeConnectTimeout_throwsException() {
assertThrows(
IllegalArgumentException.class,
() ->
new DefaultTsunamiSocketFactory(
mockSocketFactory,
mockSslSocketFactory,
Duration.ofSeconds(-1),
DEFAULT_READ_TIMEOUT));
}
@Test
public void constructor_withNegativeReadTimeout_throwsException() {
assertThrows(
IllegalArgumentException.class,
() ->
new DefaultTsunamiSocketFactory(
mockSocketFactory,
mockSslSocketFactory,
DEFAULT_CONNECT_TIMEOUT,
Duration.ofSeconds(-1)));
}
@Test
public void createSocket_withHostAndPort_setsTimeoutsAndConnects() throws IOException {
var unused = tsunamiSocketFactory.createSocket("example.com", 80);
verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis());
verify(mockSocket).setKeepAlive(true);
verify(mockSocket).setTcpNoDelay(true);
ArgumentCaptor addressCaptor =
ArgumentCaptor.forClass(InetSocketAddress.class);
ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mockSocket).connect(addressCaptor.capture(), timeoutCaptor.capture());
assertThat(addressCaptor.getValue().getHostString()).isEqualTo("example.com");
assertThat(addressCaptor.getValue().getPort()).isEqualTo(80);
assertThat(timeoutCaptor.getValue()).isEqualTo((int) DEFAULT_CONNECT_TIMEOUT.toMillis());
}
@Test
public void createSocket_withCustomTimeouts_usesProvidedValues() throws IOException {
Duration customConnect = Duration.ofSeconds(5);
Duration customRead = Duration.ofSeconds(15);
var unused = tsunamiSocketFactory.createSocket("example.com", 443, customConnect, customRead);
verify(mockSocket).setSoTimeout((int) customRead.toMillis());
ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mockSocket).connect(any(), timeoutCaptor.capture());
assertThat(timeoutCaptor.getValue()).isEqualTo((int) customConnect.toMillis());
}
@Test
public void createSocket_withInetAddress_setsTimeoutsAndConnects() throws IOException {
InetAddress address = InetAddress.getLoopbackAddress();
var unused = tsunamiSocketFactory.createSocket(address, 8080);
verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis());
verify(mockSocket).setKeepAlive(true);
verify(mockSocket).setTcpNoDelay(true);
ArgumentCaptor addressCaptor =
ArgumentCaptor.forClass(InetSocketAddress.class);
verify(mockSocket).connect(addressCaptor.capture(), anyInt());
assertThat(addressCaptor.getValue().getAddress()).isEqualTo(address);
assertThat(addressCaptor.getValue().getPort()).isEqualTo(8080);
}
@Test
public void createSocket_withInvalidPort_throwsException() {
assertThrows(
IllegalArgumentException.class, () -> tsunamiSocketFactory.createSocket("example.com", 0));
assertThrows(
IllegalArgumentException.class, () -> tsunamiSocketFactory.createSocket("example.com", -1));
assertThrows(
IllegalArgumentException.class,
() -> tsunamiSocketFactory.createSocket("example.com", 65536));
}
@Test
public void createSocket_withNullHost_throwsException() {
assertThrows(
NullPointerException.class, () -> tsunamiSocketFactory.createSocket((String) null, 80));
}
@Test
public void createUnconnectedSocket_setsReadTimeout() throws IOException {
Socket socket = tsunamiSocketFactory.createUnconnectedSocket();
assertThat(socket).isEqualTo(mockSocket);
verify(mockSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis());
}
@Test
public void createSslSocket_withHostAndPort_createsAndConfigures() throws IOException {
var unused = tsunamiSocketFactory.createSslSocket("secure.example.com", 443);
// Verify plain socket is created and connected first
verify(mockSocketFactory).createSocket();
verify(mockSocket).connect(any(InetSocketAddress.class), anyInt());
// Verify SSL wrapping
verify(mockSslSocketFactory).createSocket(mockSocket, "secure.example.com", 443, true);
verify(mockSslSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis());
verify(mockSslSocket).startHandshake();
}
@Test
public void createSslSocket_withCustomTimeouts_usesProvidedValues() throws IOException {
Duration customConnect = Duration.ofSeconds(5);
Duration customRead = Duration.ofSeconds(15);
var unused =
tsunamiSocketFactory.createSslSocket("secure.example.com", 443, customConnect, customRead);
verify(mockSocket).setSoTimeout((int) customRead.toMillis());
verify(mockSslSocket).setSoTimeout((int) customRead.toMillis());
ArgumentCaptor connectTimeoutCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mockSocket).connect(any(), connectTimeoutCaptor.capture());
assertThat(connectTimeoutCaptor.getValue()).isEqualTo((int) customConnect.toMillis());
}
@Test
public void createSslSocket_withInetAddress_createsAndConfigures() throws IOException {
InetAddress address = InetAddress.getLoopbackAddress();
var unused = tsunamiSocketFactory.createSslSocket(address, 443);
verify(mockSocketFactory).createSocket();
verify(mockSocket).connect(any(InetSocketAddress.class), anyInt());
verify(mockSslSocketFactory).createSocket(mockSocket, address.getHostAddress(), 443, true);
verify(mockSslSocket).startHandshake();
}
@Test
public void wrapWithSsl_wrapsExistingSocket() throws IOException {
when(mockSocket.getSoTimeout()).thenReturn(5000);
var unused = tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 443, true);
verify(mockSslSocketFactory).createSocket(mockSocket, "example.com", 443, true);
verify(mockSslSocket).setSoTimeout(5000);
verify(mockSslSocket).startHandshake();
}
@Test
public void wrapWithSsl_withZeroOriginalTimeout_usesDefault() throws IOException {
when(mockSocket.getSoTimeout()).thenReturn(0);
var unused = tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 443, true);
verify(mockSslSocket).setSoTimeout((int) DEFAULT_READ_TIMEOUT.toMillis());
}
@Test
public void wrapWithSsl_withNullSocket_throwsException() {
assertThrows(
NullPointerException.class,
() -> tsunamiSocketFactory.wrapWithSsl(null, "example.com", 443, true));
}
@Test
public void wrapWithSsl_withInvalidPort_throwsException() {
assertThrows(
IllegalArgumentException.class,
() -> tsunamiSocketFactory.wrapWithSsl(mockSocket, "example.com", 0, true));
}
@Test
public void getDefaultConnectTimeout_returnsConfiguredValue() {
assertThat(tsunamiSocketFactory.getDefaultConnectTimeout()).isEqualTo(DEFAULT_CONNECT_TIMEOUT);
}
@Test
public void getDefaultReadTimeout_returnsConfiguredValue() {
assertThat(tsunamiSocketFactory.getDefaultReadTimeout()).isEqualTo(DEFAULT_READ_TIMEOUT);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryCliOptionsTest.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link TsunamiSocketFactoryCliOptions}. */
@RunWith(JUnit4.class)
public final class TsunamiSocketFactoryCliOptionsTest {
@Test
public void validate_withNullValues_passes() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
// Should not throw
options.validate();
}
@Test
public void validate_withPositiveConnectTimeout_passes() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.connectTimeoutSeconds = 10;
// Should not throw
options.validate();
}
@Test
public void validate_withPositiveReadTimeout_passes() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.readTimeoutSeconds = 30;
// Should not throw
options.validate();
}
@Test
public void validate_withNegativeConnectTimeout_throwsException() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.connectTimeoutSeconds = -1;
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_withNegativeReadTimeout_throwsException() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.readTimeoutSeconds = -5;
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_withZeroConnectTimeout_throwsException() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.connectTimeoutSeconds = 0;
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_withZeroReadTimeout_throwsException() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.readTimeoutSeconds = 0;
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_withTrustAllCertificates_passes() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.trustAllCertificates = true;
// Should not throw
options.validate();
}
@Test
public void validate_withAllValidOptions_passes() {
TsunamiSocketFactoryCliOptions options = new TsunamiSocketFactoryCliOptions();
options.connectTimeoutSeconds = 10;
options.readTimeoutSeconds = 30;
options.trustAllCertificates = false;
// Should not throw
options.validate();
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/net/socket/TsunamiSocketFactoryModuleTest.java
================================================
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.net.socket;
import static com.google.common.truth.Truth.assertThat;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.ConnectTimeoutSeconds;
import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.ReadTimeoutSeconds;
import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule.TrustAllCertificates;
import java.time.Duration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link TsunamiSocketFactoryModule}. */
@RunWith(JUnit4.class)
public final class TsunamiSocketFactoryModuleTest {
private TsunamiSocketFactoryCliOptions cliOptions;
private TsunamiSocketFactoryConfigProperties configProperties;
@Before
public void setUp() {
cliOptions = new TsunamiSocketFactoryCliOptions();
configProperties = new TsunamiSocketFactoryConfigProperties();
}
@Test
public void provideTsunamiSocketFactory_returnsNonNullFactory() throws Exception {
Injector injector = Guice.createInjector(getTestingModule());
TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class);
assertThat(factory).isNotNull();
assertThat(factory).isInstanceOf(DefaultTsunamiSocketFactory.class);
}
@Test
public void provideTsunamiSocketFactory_withDefaultConfig_usesDefaultTimeouts() throws Exception {
Injector injector = Guice.createInjector(getTestingModule());
TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class);
assertThat(factory.getDefaultConnectTimeout()).isEqualTo(Duration.ofSeconds(10));
assertThat(factory.getDefaultReadTimeout()).isEqualTo(Duration.ofSeconds(30));
}
@Test
public void provideConnectTimeoutSeconds_withCliOption_usesCliValue() {
cliOptions.connectTimeoutSeconds = 20;
configProperties.connectTimeoutSeconds = 15;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(20);
}
@Test
public void provideConnectTimeoutSeconds_withConfigOnly_usesConfigValue() {
configProperties.connectTimeoutSeconds = 15;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(15);
}
@Test
public void provideConnectTimeoutSeconds_withNoConfig_usesDefault() {
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ConnectTimeoutSeconds.class))).isEqualTo(10);
}
@Test
public void provideReadTimeoutSeconds_withCliOption_usesCliValue() {
cliOptions.readTimeoutSeconds = 60;
configProperties.readTimeoutSeconds = 45;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(60);
}
@Test
public void provideReadTimeoutSeconds_withConfigOnly_usesConfigValue() {
configProperties.readTimeoutSeconds = 45;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(45);
}
@Test
public void provideReadTimeoutSeconds_withNoConfig_usesDefault() {
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(int.class, ReadTimeoutSeconds.class))).isEqualTo(30);
}
@Test
public void provideTrustAllCertificates_withCliOptionTrue_returnsTrue() {
cliOptions.trustAllCertificates = true;
configProperties.trustAllCertificates = false;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isTrue();
}
@Test
public void provideTrustAllCertificates_withCliOptionFalse_returnsFalse() {
cliOptions.trustAllCertificates = false;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isFalse();
}
@Test
public void provideTrustAllCertificates_withConfigOnly_usesConfigValue() {
configProperties.trustAllCertificates = false;
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isFalse();
}
@Test
public void provideTrustAllCertificates_withNoConfig_usesDefaultTrue() {
Injector injector = Guice.createInjector(getTestingModule());
assertThat(injector.getInstance(Key.get(boolean.class, TrustAllCertificates.class))).isTrue();
}
@Test
public void provideTsunamiSocketFactory_withCustomConfig_usesCustomValues() throws Exception {
cliOptions.connectTimeoutSeconds = 5;
cliOptions.readTimeoutSeconds = 15;
Injector injector = Guice.createInjector(getTestingModule());
TsunamiSocketFactory factory = injector.getInstance(TsunamiSocketFactory.class);
assertThat(factory.getDefaultConnectTimeout()).isEqualTo(Duration.ofSeconds(5));
assertThat(factory.getDefaultReadTimeout()).isEqualTo(Duration.ofSeconds(15));
}
private AbstractModule getTestingModule() {
return new AbstractModule() {
@Override
protected void configure() {
install(new TsunamiSocketFactoryModule());
bind(TsunamiSocketFactoryCliOptions.class).toInstance(cliOptions);
bind(TsunamiSocketFactoryConfigProperties.class).toInstance(configProperties);
}
};
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/server/CompactRunRequestHelperTest.java
================================================
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.server;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.MatchedPlugin;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.NetworkService;
import com.google.tsunami.proto.PluginDefinition;
import com.google.tsunami.proto.PluginInfo;
import com.google.tsunami.proto.RunCompactRequest;
import com.google.tsunami.proto.RunCompactRequest.PluginNetworkServiceTarget;
import com.google.tsunami.proto.RunRequest;
import com.google.tsunami.proto.TargetInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class CompactRunRequestHelperTest {
@Test
public void compressingRunRequest_isMoreCompact() {
NetworkService service1 = NetworkService.newBuilder().setServiceName("service1").build();
NetworkService service2 = NetworkService.newBuilder().setServiceName("service2").build();
PluginDefinition plugin1 =
PluginDefinition.newBuilder()
.setInfo(PluginInfo.newBuilder().setName("plugin1").build())
.build();
PluginDefinition plugin2 =
PluginDefinition.newBuilder()
.setInfo(PluginInfo.newBuilder().setName("plugin2").build())
.build();
PluginDefinition plugin3 =
PluginDefinition.newBuilder()
.setInfo(PluginInfo.newBuilder().setName("plugin3").build())
.build();
MatchedPlugin matchedPlugin1 =
MatchedPlugin.newBuilder().addServices(service1).setPlugin(plugin1).build();
MatchedPlugin matchedPlugin2 =
MatchedPlugin.newBuilder().addServices(service2).setPlugin(plugin2).build();
MatchedPlugin matchedPlugin3 =
MatchedPlugin.newBuilder().addServices(service1).setPlugin(plugin3).build();
ImmutableList expectedMatchedPlugins =
ImmutableList.of(matchedPlugin1, matchedPlugin2, matchedPlugin3);
TargetInfo expectedTargetInfo =
TargetInfo.newBuilder()
.addNetworkEndpoints(
NetworkEndpoint.newBuilder()
.setHostname(Hostname.newBuilder().setName("example.com").build())
.build())
.build();
RunRequest expectedUncompressedRunRequest =
RunRequest.newBuilder()
.setTarget(expectedTargetInfo)
.addAllPlugins(expectedMatchedPlugins)
.build();
var actualCompressedRunRequest =
CompactRunRequestHelper.compress(expectedUncompressedRunRequest);
var expectedCompressedRunRequest =
RunCompactRequest.newBuilder()
.setTarget(expectedTargetInfo)
.addServices(service1)
.addServices(service2)
.addPlugins(plugin1)
.addPlugins(plugin2)
.addPlugins(plugin3)
.addScanTargets(
PluginNetworkServiceTarget.newBuilder()
.setPluginIndex(0)
.setServiceIndex(0)
.build())
.addScanTargets(
PluginNetworkServiceTarget.newBuilder()
.setPluginIndex(1)
.setServiceIndex(1)
.build())
.addScanTargets(
PluginNetworkServiceTarget.newBuilder()
.setPluginIndex(2)
.setServiceIndex(0)
.build())
.build();
assertThat(actualCompressedRunRequest).isEqualTo(expectedCompressedRunRequest);
// And now uncompressing it again:
var actualUncompressedRunRequest =
CompactRunRequestHelper.uncompress(actualCompressedRunRequest);
// It should match the original setup
assertThat(actualUncompressedRunRequest).isEqualTo(expectedUncompressedRunRequest);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/time/SystemUtcClockModuleTest.java
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time;
import static com.google.common.truth.Truth.assertThat;
import com.google.inject.Guice;
import com.google.inject.Key;
import java.time.Clock;
import java.time.ZoneOffset;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link SystemUtcClockModule}. */
@RunWith(JUnit4.class)
public class SystemUtcClockModuleTest {
@Test
public void configure_always_bindsClockToSystemUtc() {
Clock clock =
Guice.createInjector(new SystemUtcClockModule())
.getInstance(Key.get(Clock.class, UtcClock.class));
assertThat(clock).isNotNull();
assertThat(clock.getZone()).isEqualTo(ZoneOffset.UTC);
// A hacky way of testing the instance is a SystemClock.
assertThat(clock.toString()).contains("SystemClock");
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/time/testing/FakeUtcClockModuleTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time.testing;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.tsunami.common.time.UtcClock;
import java.time.Clock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link FakeUtcClockModule}. */
@RunWith(JUnit4.class)
public class FakeUtcClockModuleTest {
@Test
public void constructor_withNullFakeClock_throwsNullPointerException() {
assertThrows(NullPointerException.class, () -> new FakeUtcClockModule(null));
}
@Test
public void configure_always_bindsToSameInstance() {
FakeUtcClock fakeUtcClock = FakeUtcClock.create();
Injector injector = Guice.createInjector(new FakeUtcClockModule(fakeUtcClock));
assertThat(injector.getInstance(Key.get(Clock.class, UtcClock.class)))
.isSameInstanceAs(fakeUtcClock);
assertThat(injector.getInstance(Key.get(Clock.class, UtcClock.class)))
.isSameInstanceAs(fakeUtcClock);
assertThat(injector.getInstance(Key.get(FakeUtcClock.class, UtcClock.class)))
.isSameInstanceAs(fakeUtcClock);
assertThat(injector.getInstance(Key.get(FakeUtcClock.class, UtcClock.class)))
.isSameInstanceAs(fakeUtcClock);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/time/testing/FakeUtcClockTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.time.testing;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import java.time.Duration;
import java.time.Instant;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link FakeUtcClock}. */
@RunWith(JUnit4.class)
public class FakeUtcClockTest {
private static final Instant TEST_INSTANT = Instant.ofEpochMilli(1927081738591L);
private static final Duration TEST_DURATION = Duration.ofSeconds(20);
@Test
public void setNow_always_setsClockToGivenInstant() {
assertThat(FakeUtcClock.create().setNow(TEST_INSTANT).instant()).isEqualTo(TEST_INSTANT);
}
@Test
public void setNow_whenCallWithNull_throwsNullPointerException() {
assertThrows(NullPointerException.class, () -> FakeUtcClock.create().setNow(null));
}
@Test
public void advance_always_advancesGivenDuration() {
FakeUtcClock fakeUtcClock = FakeUtcClock.create().setNow(TEST_INSTANT);
FakeUtcClock advancedClock = fakeUtcClock.advance(TEST_DURATION);
assertThat(advancedClock.instant()).isEqualTo(TEST_INSTANT.plus(TEST_DURATION));
}
@Test
public void advance_whenCalledWithNull_throwsNullPointerException() {
assertThrows(NullPointerException.class, () -> FakeUtcClock.create().advance(null));
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/ComparisonUtilityTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link ComparisonUtility}. */
@RunWith(JUnit4.class)
public final class ComparisonUtilityTest {
@Test
public void compareWithFillValue_bothEmptyListWithFillValueEqualToZero_returnsZero() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(), Lists.newArrayList(), 0))
.isEqualTo(0);
}
@Test
public void compareWithFillValue_bothEmptyListWithPositiveFillValue_returnsZero() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(), Lists.newArrayList(), 1))
.isEqualTo(0);
}
@Test
public void compareWithFillValue_bothEmptyListWithNegativeFilValue_returnsZero() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(), Lists.newArrayList(), -1))
.isEqualTo(0);
}
@Test
public void compareWithFillValue_oneEmptyListAndSmallFillValue_returnsNegative() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(), Lists.newArrayList(1, 2, 3), 0))
.isLessThan(0);
}
@Test
public void compareWithFillValue_oneEmptyListAndLargeFillValue_returnsPositive() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(), Lists.newArrayList(1, 2, 3), 100))
.isGreaterThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListSameSizeGreaterValue_returnsPositive() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 3, 4), Lists.newArrayList(1, 2, 3), 100))
.isGreaterThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListSameSizeEqualValue_returnsZero() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 2, 3), Lists.newArrayList(1, 2, 3), 100))
.isEqualTo(0);
}
@Test
public void compareWithFillValue_nonEmptyListSameSizeLessThanValue_returnsNegative() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 1, 3), Lists.newArrayList(1, 2, 3), 100))
.isLessThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListVariedSizeWithPositiveFillValue_returnsNegative() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 1), Lists.newArrayList(1, 2, 3), 100))
.isLessThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListVariedSizeWithZeroFillValue_returnsPositive() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 3), Lists.newArrayList(1, 2, 3), 0))
.isGreaterThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListVariedSizeWithZeroFillValue_returnsNegative() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 2), Lists.newArrayList(1, 2, 3), 0))
.isLessThan(0);
}
@Test
public void compareWithFillValue_nonEmptyListVariedSizeWithPositiveFillValue_returnsPositive() {
assertThat(
ComparisonUtility.compareListWithFillValue(
Lists.newArrayList(1, 2), Lists.newArrayList(1, 2, 3), 100))
.isGreaterThan(0);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/EqualsTestCase.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import com.google.auto.value.AutoValue;
import com.google.errorprone.annotations.Immutable;
@Immutable(containerOf = "T")
@AutoValue
abstract class EqualsTestCase {
abstract T first();
abstract T second();
static EqualsTestCase create(T first, T second) {
return new AutoValue_EqualsTestCase<>(first, second);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/KnownQualifierTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.EnumSet;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/** Tests for {@link KnownQualifier}. */
@RunWith(Theories.class)
public final class KnownQualifierTest {
@DataPoints("ValidKnownQualifierText")
public static ImmutableList validTextsForKnownQualifiers() {
return Arrays.stream(KnownQualifier.values())
.map(KnownQualifier::getQualifierText)
.collect(ImmutableList.toImmutableList());
}
@Test
public void compareTo_always_hasTheCorrectOrder() {
// KnownQualifier should promise the following order to callers.
assertThat(EnumSet.allOf(KnownQualifier.class))
.containsExactly(
KnownQualifier.ALPHA,
KnownQualifier.BETA,
KnownQualifier.PRE,
KnownQualifier.R,
KnownQualifier.RC,
KnownQualifier.ABSENT,
KnownQualifier.P,
KnownQualifier.PATCH,
KnownQualifier.PATCHED)
.inOrder();
}
@Theory
public void isKnownQualifier_validText_returnsTrue(
@FromDataPoints("ValidKnownQualifierText") String validText) {
assertThat(KnownQualifier.isKnownQualifier(validText)).isTrue();
}
@Theory
public void isKnownQualifier_invalidText_returnsFalse() {
assertThat(KnownQualifier.isKnownQualifier("random")).isFalse();
}
@Theory
public void fromText_validText_returnsKnownQualifier(
@FromDataPoints("ValidKnownQualifierText") String validText) {
assertThat(KnownQualifier.fromText(validText)).isIn(EnumSet.allOf(KnownQualifier.class));
}
@Test
public void fromText_invalidText_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> KnownQualifier.fromText("random"));
}
@Test
public void fromText_invalidTextUsingComposedValidTokens_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> KnownQualifier.fromText("alpha-beta"));
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/LessThanTestCase.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import com.google.auto.value.AutoValue;
import com.google.errorprone.annotations.Immutable;
@Immutable(containerOf = "T")
@AutoValue
abstract class LessThanTestCase {
abstract T smaller();
abstract T larger();
static LessThanTestCase create(T smaller, T larger) {
return new AutoValue_LessThanTestCase<>(smaller, larger);
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/SegmentTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/** Tests for {@link Segment}. */
@RunWith(Theories.class)
public final class SegmentTest {
@Test
public void fromTokenList_startsWithKnownQualifier_buildsFromInput() {
ImmutableList tokens =
ImmutableList.of(Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.fromNumeric(1L));
Segment segment = Segment.fromTokenList(tokens);
assertThat(segment.tokens()).containsExactlyElementsIn(tokens);
}
@Test
public void fromTokenList_noKnownQualifier_addsKnownQualifier() {
ImmutableList tokens = ImmutableList.of(Token.fromText("abc"), Token.fromNumeric(1L));
Segment segment = Segment.fromTokenList(tokens);
assertThat(segment.tokens())
.containsExactlyElementsIn(
Stream.concat(
Stream.of(Token.fromKnownQualifier(KnownQualifier.ABSENT)), tokens.stream())
.collect(Collectors.toList()));
}
@Test
public void fromString_emptyString_returnsNullSegment() {
assertThat(Segment.fromString("")).isEqualTo(Segment.NULL);
}
@Test
public void fromString_allExcludedTokens_returnsNullSegment() {
assertThat(Segment.fromString("gg.N/A")).isEqualTo(Segment.NULL);
}
@Test
public void fromString_allEmptyTokens_returnsNullSegment() {
assertThat(Segment.fromString("...")).isEqualTo(Segment.NULL);
}
@Test
public void fromString_textAndNumeric_returnsSeparatedTextAndNumber() {
assertThat(Segment.fromString("abc1.0").tokens())
.containsExactly(
Token.fromKnownQualifier(KnownQualifier.ABSENT),
Token.fromText("abc"),
Token.fromNumeric(1L),
Token.fromNumeric(0L));
assertThat(Segment.fromString("gg1.0").tokens())
.containsExactly(
Token.fromKnownQualifier(KnownQualifier.ABSENT),
Token.fromNumeric(1L),
Token.fromNumeric(0L));
}
@Test
public void fromString_noKnownQualifier_addsKnownQualifier() {
assertThat(Segment.fromString("2.1.1").tokens())
.containsExactly(
Token.fromKnownQualifier(KnownQualifier.ABSENT),
Token.fromNumeric(2L),
Token.fromNumeric(1L),
Token.fromNumeric(1L));
}
@Test
public void fromString_startsWithKnownQualifier_parsesNumericAndTextTokens() {
assertThat(Segment.fromString("alpha.1~text").tokens())
.containsExactly(
Token.fromKnownQualifier(KnownQualifier.ALPHA),
Token.fromNumeric(1L),
Token.fromText("~"),
Token.fromText("text"));
}
@DataPoints("Equals")
public static ImmutableList> equalTestCases() {
return ImmutableList.of(
EqualsTestCase.create(Segment.fromString(""), Segment.fromString("")),
EqualsTestCase.create(Segment.fromString(""), Segment.fromString("gg.N/A")),
EqualsTestCase.create(Segment.fromString("1.1"), Segment.fromString("1.1")),
EqualsTestCase.create(Segment.fromString("1.1-"), Segment.fromString("1.1-gg")));
}
@Theory
public void compareTo_equalTestCase_returnsZero(
@FromDataPoints("Equals") EqualsTestCase testCase) {
assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second());
}
@DataPoints("LessThan")
public static ImmutableList> lessThanTestCases() {
return ImmutableList.of(
// Null token.
LessThanTestCase.create(Segment.fromString("2.1"), Segment.fromString("2.1.1")),
LessThanTestCase.create(Segment.fromString("alpha"), Segment.fromString("")),
// Numeric token.
LessThanTestCase.create(Segment.fromString("2.1"), Segment.fromString("2.2")),
LessThanTestCase.create(Segment.fromString("0.9"), Segment.fromString("1.0")),
// Known qualifiers.
LessThanTestCase.create(Segment.fromString("alpha"), Segment.fromString("beta")),
LessThanTestCase.create(Segment.fromString("alpha.beta"), Segment.fromString("alpha")),
LessThanTestCase.create(Segment.fromString("alpha.beta"), Segment.fromString("alpha.rc")),
// Text token.
LessThanTestCase.create(Segment.fromString("abc"), Segment.fromString("def")),
LessThanTestCase.create(Segment.fromString("abc.def"), Segment.fromString("abc.ghi")),
LessThanTestCase.create(Segment.fromString("abc"), Segment.fromString("DEF")),
// Mixed type.
LessThanTestCase.create(Segment.fromString("2.1.1"), Segment.fromString("2.1.abc")));
}
@Theory
public void compareTo_lessThanTestCase_hasCorrectSymmetryResult(
@FromDataPoints("LessThan") LessThanTestCase testCase) {
assertThat(testCase.smaller()).isLessThan(testCase.larger());
assertThat(testCase.larger()).isGreaterThan(testCase.smaller());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/TokenTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/** Tests for {@link Token}. */
@RunWith(Theories.class)
public final class TokenTest {
@Test
public void fromNumeric_always_returnsNumericToken() {
Token numericToken = Token.fromNumeric(123L);
assertThat(numericToken.isNumeric()).isTrue();
assertThat(numericToken.getNumeric()).isEqualTo(123L);
}
@Test
public void fromText_always_returnsTextToken() {
Token textToken = Token.fromText("abc");
assertThat(textToken.isText()).isTrue();
assertThat(textToken.isKnownQualifier()).isFalse();
assertThat(textToken.getText()).isEqualTo("abc");
}
@Test
public void fromKnownQualifier_always_returnsTextToken() {
Token textToken = Token.fromKnownQualifier(KnownQualifier.ALPHA);
assertThat(textToken.isText()).isTrue();
assertThat(textToken.isKnownQualifier()).isTrue();
assertThat(textToken.getText()).isEqualTo(KnownQualifier.ALPHA.getQualifierText());
}
@DataPoints("Equal")
public static ImmutableList> equalTestCases() {
return ImmutableList.of(
EqualsTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.ABSENT)),
EqualsTestCase.create(Token.EMPTY, Token.fromText("")),
EqualsTestCase.create(Token.fromNumeric(1L), Token.fromNumeric(1L)),
EqualsTestCase.create(
Token.fromKnownQualifier(KnownQualifier.P), Token.fromKnownQualifier(KnownQualifier.P)),
EqualsTestCase.create(Token.fromKnownQualifier(KnownQualifier.P), Token.fromText("p")),
EqualsTestCase.create(Token.fromKnownQualifier(KnownQualifier.P), Token.fromText("P")),
EqualsTestCase.create(Token.fromText("abc"), Token.fromText("ABC")));
}
@Theory
public void isEqualTo_equalTestCase_returnsZero(
@FromDataPoints("Equal") EqualsTestCase testCase) {
assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second());
}
@DataPoints("LessThan")
public static ImmutableList> lessThanTestCases() {
return ImmutableList.of(
// Empty token and Numeric token test cases.
LessThanTestCase.create(Token.EMPTY, Token.fromNumeric(1L)),
LessThanTestCase.create(Token.EMPTY, Token.fromNumeric(0L)),
// Empty token and KnownQualifier test cases.
LessThanTestCase.create(Token.fromKnownQualifier(KnownQualifier.ALPHA), Token.EMPTY),
LessThanTestCase.create(Token.fromKnownQualifier(KnownQualifier.RC), Token.EMPTY),
LessThanTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.P)),
LessThanTestCase.create(Token.EMPTY, Token.fromKnownQualifier(KnownQualifier.PATCH)),
// Empty token and Text token test case.
LessThanTestCase.create(Token.EMPTY, Token.fromText("abc")),
LessThanTestCase.create(Token.EMPTY, Token.fromText("123")),
// Numeric token only test case.
LessThanTestCase.create(Token.fromNumeric(0L), Token.fromNumeric(1L)),
// KnownQualifier only test case.
LessThanTestCase.create(
Token.fromKnownQualifier(KnownQualifier.ALPHA),
Token.fromKnownQualifier(KnownQualifier.ABSENT)),
LessThanTestCase.create(
Token.fromKnownQualifier(KnownQualifier.ABSENT),
Token.fromKnownQualifier(KnownQualifier.PATCH)),
// Text only test case.
LessThanTestCase.create(Token.fromText("abc"), Token.fromText("def")),
LessThanTestCase.create(Token.fromText("abc"), Token.fromText("dEf")),
LessThanTestCase.create(
Token.fromText("abc"), Token.fromKnownQualifier(KnownQualifier.ALPHA)),
// Numeric and Text test case
LessThanTestCase.create(Token.fromNumeric(1L), Token.fromText("abc")),
LessThanTestCase.create(
Token.fromNumeric(1L), Token.fromKnownQualifier(KnownQualifier.ALPHA)));
}
@Theory
public void compareTo_lessThanTestCase_hasCorrectSymmetryResult(
@FromDataPoints("LessThan") LessThanTestCase testCase) {
// Test symmetry.
assertThat(testCase.smaller()).isLessThan(testCase.larger());
assertThat(testCase.larger()).isGreaterThan(testCase.smaller());
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/VersionRangeTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.tsunami.common.version.VersionRange.Inclusiveness;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link VersionRange}. */
@RunWith(JUnit4.class)
public class VersionRangeTest {
@Test
public void parse_withNegativeInfinityRange_returnsCorrectVersionRange() {
VersionRange versionRange = VersionRange.parse("(,1.0]");
assertThat(versionRange)
.isEqualTo(
VersionRange.builder()
.setMinVersion(Version.minimum())
.setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE)
.setMaxVersion(Version.fromString("1.0"))
.setMaxVersionInclusiveness(Inclusiveness.INCLUSIVE)
.build());
}
@Test
public void parse_withPositiveInfinityRange_returnsCorrectVersionRange() {
VersionRange versionRange = VersionRange.parse("(1.0,)");
assertThat(versionRange)
.isEqualTo(
VersionRange.builder()
.setMinVersion(Version.fromString("1.0"))
.setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE)
.setMaxVersion(Version.maximum())
.setMaxVersionInclusiveness(Inclusiveness.EXCLUSIVE)
.build());
}
@Test
public void parse_withRegularRange_returnsCorrectVersionRange() {
VersionRange versionRange = VersionRange.parse("(1.0,2.0]");
assertThat(versionRange)
.isEqualTo(
VersionRange.builder()
.setMinVersion(Version.fromString("1.0"))
.setMinVersionInclusiveness(Inclusiveness.EXCLUSIVE)
.setMaxVersion(Version.fromString("2.0"))
.setMaxVersionInclusiveness(Inclusiveness.INCLUSIVE)
.build());
}
@Test
public void parse_withEmptyRangeString_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse(""));
assertThat(exception).hasMessageThat().isEqualTo("Range string cannot be empty.");
}
@Test
public void parse_withRangeNotStartingWithParenthesis_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse(",1.0]"));
assertThat(exception)
.hasMessageThat()
.isEqualTo("Version range must start with '[' or '(', got ',1.0]'");
}
@Test
public void parse_withRangeNotEndingWithParenthesis_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,1.0"));
assertThat(exception)
.hasMessageThat()
.isEqualTo("Version range must end with ']' or ')', got '(,1.0'");
}
@Test
public void parse_withTooManyParenthesis_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,1.0]]"));
assertThat(exception)
.hasMessageThat()
.isEqualTo("Parenthesis and/or brackets not allowed within version range, got '(,1.0]]'");
}
@Test
public void parse_withTooManyCommas_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,,,1.0]"));
assertThat(exception).hasMessageThat().isEqualTo("Invalid range of versions, got '(,,,1.0]'");
}
@Test
public void parse_withoutComma_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("[1.0]"));
assertThat(exception).hasMessageThat().isEqualTo("Invalid range of versions, got '[1.0]'");
}
@Test
public void parse_withMinimalToMaximalRange_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("(,)"));
assertThat(exception).hasMessageThat().isEqualTo("Infinity range is not supported, got '(,)'");
}
@Test
public void parse_withTheSameRangeEnds_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionRange.parse("[1.0,1.0]"));
assertThat(exception)
.hasMessageThat()
.isEqualTo("Min version in range must be less than max version in range, got '[1.0,1.0]'");
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/VersionSetTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link VersionSet}. */
@RunWith(JUnit4.class)
public class VersionSetTest {
@Test
public void parse_withValidVersionsAndVersionRanges_returnsParsedVersionSet() {
VersionSet versionSet =
VersionSet.parse(ImmutableList.of("1.0", "1.3", "(1.4, 1.7]", "1.9", "[2.0,)"));
assertThat(versionSet.versions())
.containsExactly(
Version.fromString("1.0"), Version.fromString("1.3"), Version.fromString("1.9"));
assertThat(versionSet.versionRanges())
.containsExactly(VersionRange.parse("(1.4,1.7]"), VersionRange.parse("[2.0,)"));
}
@Test
public void parse_withEmptyInputList_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> VersionSet.parse(ImmutableList.of()));
assertThat(exception).hasMessageThat().isEqualTo("Versions and ranges list cannot be empty.");
}
@Test
public void parse_withInvalidVersion_throwsIllegalArgumentException() {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class, () -> VersionSet.parse(ImmutableList.of("1,0", "abc")));
assertThat(exception)
.hasMessageThat()
.isEqualTo("String '1,0' is neither a discrete string nor a version range.");
}
}
================================================
FILE: common/src/test/java/com/google/tsunami/common/version/VersionTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.common.version;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/** Tests for {@link Version}. */
@RunWith(Theories.class)
public final class VersionTest {
@Test
public void create_whenNormalVersionAndValueIsNull_throwsException() {
assertThrows(IllegalArgumentException.class, () -> Version.fromString(null));
}
@Test
public void create_whnNormalVersionAndValueIsEmpty_throwsExceptionIfStringIsNull() {
assertThrows(IllegalArgumentException.class, () -> Version.fromString(""));
}
@Test
public void create_whenNormalVersion_returnsTypeNormal() {
Version version = Version.fromString("1.1");
assertThat(version.versionType()).isEqualTo(Version.Type.NORMAL);
}
@Test
public void createMaximum_always_returnsTypeMaximum() {
Version version = Version.maximum();
assertThat(version.versionType()).isEqualTo(Version.Type.MAXIMUM);
}
@Test
public void createMaximum_always_returnsTypeMinimum() {
Version version = Version.minimum();
assertThat(version.versionType()).isEqualTo(Version.Type.MINIMUM);
}
@DataPoints("InvalidVersion")
public static ImmutableList invalidVersionTestCases() {
return ImmutableList.of("", "N/A", "...", "-");
}
@Theory
public void fromString_invalidVersionString_throwsIllegalArgumentException(
@FromDataPoints("InvalidVersion") String version) {
assertThrows(IllegalArgumentException.class, () -> Version.fromString(version));
}
@Test
public void fromString_validString_storesInputAsRawString() {
assertThat(Version.fromString("1.0").versionString()).isEqualTo("1.0");
}
@Test
public void fromString_noEpoch_appendsZeroEpoch() {
assertThat(Version.fromString("1.0").segments())
.containsExactly(Segment.fromString("0"), Segment.fromString("1.0"));
}
@Test
public void fromString_withEpoch_epochIsParsed() {
assertThat(Version.fromString("1:1.0").segments())
.containsExactly(Segment.fromString("1"), Segment.fromString("1.0"));
}
@Test
public void fromString_withMultipleSegments_segmentsParsedCorrectly() {
assertThat(Version.fromString("1:9.7.0.dfsg.P1-gg3.0").segments())
.containsExactly(
Segment.fromString("1"),
Segment.fromString("9.7.0.dfsg.P1"),
Segment.fromString("3.0"));
}
@DataPoints("Equals")
public static ImmutableList> equalsTestCases() {
return ImmutableList.of(
EqualsTestCase.create(Version.fromString("1.0"), Version.fromString("1.0")),
EqualsTestCase.create(Version.fromString("1.0"), Version.fromString("0:1.0")),
EqualsTestCase.create(Version.fromString("1.0-"), Version.fromString("1.0-gg")),
EqualsTestCase.create(Version.maximum(), Version.maximum()),
EqualsTestCase.create(Version.minimum(), Version.minimum()));
}
@Theory
public void compareTo_equalsTestCase_returnsZero(
@FromDataPoints("Equals") EqualsTestCase testCase) {
assertThat(testCase.first()).isEquivalentAccordingToCompareTo(testCase.second());
}
@DataPoints("LessThan")
public static ImmutableList> lessThanTestCases() {
return ImmutableList.of(
LessThanTestCase.create(Version.minimum(), Version.fromString("1.0")),
LessThanTestCase.create(Version.minimum(), Version.maximum()),
LessThanTestCase.create(Version.fromString("1.0"), Version.maximum()),
LessThanTestCase.create(Version.fromString("0.9"), Version.fromString("1.0")),
LessThanTestCase.create(Version.fromString("1.0-0"), Version.fromString("1.0-110313082")),
LessThanTestCase.create(
Version.fromString("0.161-gg2.0"), Version.fromString("0.165-gg1.0")),
LessThanTestCase.create(Version.fromString("0.87-gg1.2"), Version.fromString("0.87-gg1.3")),
LessThanTestCase.create(
Version.fromString("2017b-gg1.0"), Version.fromString("2018b-gg0.")),
LessThanTestCase.create(Version.fromString("18-4"), Version.fromString("19-1")),
LessThanTestCase.create(
Version.fromString("1:9.7.0.dfsg.P1-gg3.0"),
Version.fromString("1:9.8.0.dfsg.P0-gg1.0")),
LessThanTestCase.create(Version.fromString("5.7-gg2.0"), Version.fromString("5.8-gg1.0")),
LessThanTestCase.create(
Version.fromString("2.4.6-12"), Version.fromString("2.4.6-12+patched1")),
LessThanTestCase.create(Version.fromString("20170727-1"), Version.fromString("20170801-1")),
LessThanTestCase.create(
Version.fromString("3.12-443-ga51ea6dc8202-gg2.2"),
Version.fromString("3.13-440-ga51eadfe472-gg1.0")),
LessThanTestCase.create(Version.fromString("1.0a"), Version.fromString("1.0b")),
LessThanTestCase.create(Version.fromString("1a"), Version.fromString("2")),
LessThanTestCase.create(
Version.fromString("4d01146f1679dd90bba45adb60d24ad11fe1155e"),
Version.fromString("e40b437401966fe06b0c4d5430c35e4494675c90")),
LessThanTestCase.create(Version.fromString("7.10"), Version.fromString("1_7.9")),
LessThanTestCase.create(Version.fromString("9.10"), Version.fromString("1_9.11")),
LessThanTestCase.create(
Version.fromString("1.0.0-alpha"), Version.fromString("1.0.0-alpha.1")),
LessThanTestCase.create(
Version.fromString("1.0.0-alpha.1"), Version.fromString("1.0.0-alpha.beta")),
LessThanTestCase.create(
Version.fromString("1.0.0-alpha.beta"), Version.fromString("1.0.0-beta")),
LessThanTestCase.create(
Version.fromString("1.0.0-beta"), Version.fromString("1.0.0-beta.2")),
LessThanTestCase.create(
Version.fromString("1.0.0-beta.2"), Version.fromString("1.0.0-beta.11")),
LessThanTestCase.create(
Version.fromString("1.0.0-beta.11"), Version.fromString("1.0.0-rc.1")),
LessThanTestCase.create(Version.fromString("1.0.0-rc.1"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("1.0.0-alpha"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("1.0.0-alpha.1"), Version.fromString("1.0.0")),
LessThanTestCase.create(
Version.fromString("1.0.0-alpha.beta"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("1.0.0-beta"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("1.0.0-beta.2"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("1.0.0-beta.11"), Version.fromString("1.0.0")),
LessThanTestCase.create(Version.fromString("7.6-0"), Version.fromString("7.6p2-4")),
LessThanTestCase.create(Version.fromString("1.0-1"), Version.fromString("1.0.3-3")),
LessThanTestCase.create(Version.fromString("1.2.2-2"), Version.fromString("1.3")),
LessThanTestCase.create(Version.fromString("1.2.2"), Version.fromString("1.3")),
LessThanTestCase.create(Version.fromString("0-pre"), Version.fromString("0-pree")),
LessThanTestCase.create(Version.fromString("1.1.6r-1"), Version.fromString("1.1.6r2-2")),
LessThanTestCase.create(Version.fromString("2.6b-2"), Version.fromString("2.6b2-1")),
LessThanTestCase.create(
Version.fromString("98.1-pre2-b6-2"), Version.fromString("98.1p5-1")),
LessThanTestCase.create(Version.fromString("0.4-1"), Version.fromString("0.4a6-2")),
LessThanTestCase.create(Version.fromString("1:3.0.5-2"), Version.fromString("1:3.0.5.1")),
LessThanTestCase.create(Version.fromString("10.3"), Version.fromString("1:0.4")),
LessThanTestCase.create(Version.fromString("1:1.25-4"), Version.fromString("1:1.25-8")),
LessThanTestCase.create(Version.fromString("1.18.35"), Version.fromString("1.18.36")),
LessThanTestCase.create(Version.fromString("1.18.35"), Version.fromString("0:1.18.36")),
LessThanTestCase.create(
Version.fromString("9:1.18.36:5.4-20"), Version.fromString("10:0.5.1-22")),
LessThanTestCase.create(
Version.fromString("9:1.18.36:5.4-20"), Version.fromString("9:1.18.36:5.5-1")),
LessThanTestCase.create(
Version.fromString("9:1.18.36:5.4-20"), Version.fromString("9:1.18.37:4.3-22")),
LessThanTestCase.create(
Version.fromString("1.18.36-0.17.35-18"), Version.fromString("1.18.36-19")),
LessThanTestCase.create(
Version.fromString("1:1.2.13-3"), Version.fromString("1:1.2.13-3.1")),
LessThanTestCase.create(Version.fromString("2.0.7pre1-4"), Version.fromString("2.0.7r-1")),
LessThanTestCase.create(Version.fromString("0.2"), Version.fromString("1.0-0")),
LessThanTestCase.create(Version.fromString("1.0"), Version.fromString("1.0-0+b1")),
LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.3-1")),
LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.4")),
LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.4")),
LessThanTestCase.create(Version.fromString("1.2.3"), Version.fromString("1.2.24")),
LessThanTestCase.create(Version.fromString("0.8.7"), Version.fromString("0.10.0")),
LessThanTestCase.create(Version.fromString("2.3"), Version.fromString("3.2")),
LessThanTestCase.create(Version.fromString("1.3.2"), Version.fromString("1.3.2a")),
LessThanTestCase.create(Version.fromString("0.5.0~git"), Version.fromString("0.5.0~git2")),
LessThanTestCase.create(Version.fromString("2a"), Version.fromString("21")),
LessThanTestCase.create(Version.fromString("1.3.2a"), Version.fromString("1.3.2b")),
LessThanTestCase.create(Version.fromString("1.2.4"), Version.fromString("1:1.2.3")),
LessThanTestCase.create(Version.fromString("1:1.2.3"), Version.fromString("1:1.2.4")),
LessThanTestCase.create(Version.fromString("1.2a+~bCd3"), Version.fromString("1.2a++")),
LessThanTestCase.create(Version.fromString("1.2a+~"), Version.fromString("1.2a+~bCd3")),
LessThanTestCase.create(Version.fromString("304-2"), Version.fromString("5:2")),
LessThanTestCase.create(Version.fromString("5:2"), Version.fromString("304:2")),
LessThanTestCase.create(Version.fromString("3:2"), Version.fromString("25:2")),
LessThanTestCase.create(Version.fromString("1:2:123"), Version.fromString("1:12:3")),
LessThanTestCase.create(Version.fromString("1.2-3-5"), Version.fromString("1.2-5")),
LessThanTestCase.create(Version.fromString("5.005"), Version.fromString("5.10.0")),
LessThanTestCase.create(Version.fromString("3.10.2"), Version.fromString("3a9.8")),
LessThanTestCase.create(Version.fromString("3~10"), Version.fromString("3a9.8")),
LessThanTestCase.create(
Version.fromString("1.4+OOo3.0.0~"), Version.fromString("1.4+OOo3.0.0-4")),
LessThanTestCase.create(Version.fromString("3.0~rc1-1"), Version.fromString("3.0-1")),
LessThanTestCase.create(Version.fromString("2.4.7-1"), Version.fromString("2.4.7-z")),
LessThanTestCase.create(Version.fromString("1.00"), Version.fromString("1.002-1+b2")),
LessThanTestCase.create(Version.fromString("5.36-r0"), Version.fromString("5.36")),
LessThanTestCase.create(Version.fromString("5.36-r0"), Version.fromString("5.36-gg1.0")),
LessThanTestCase.create(
Version.fromString("5.36-r0"), Version.fromString("5.36-r0-gg1.0")));
}
@Theory
public void compareTo_lessThanTestCase_hasCorrectSymmetryResult(
@FromDataPoints("LessThan") LessThanTestCase testCase) {
assertThat(testCase.smaller()).isLessThan(testCase.larger());
assertThat(testCase.larger()).isGreaterThan(testCase.smaller());
}
}
================================================
FILE: common/src/test/resources/com/google/tsunami/common/net/http/testdata/README.md
================================================
# HTTP Lib Testdata
## tsunami_test_server.p12
This is a PKCS12 self-signed server key/cert file. This file was generated using
the following commands with the password `tsunamitest`:
```shell
$ openssl req -new -x509 -nodes -sha1 -days 3650 \
-out /tmp/tsunami_test_server.crt \
-keyout /tmp/tsunami_test_server.key
# Password is "tsunamitest" without the quotes.
$ openssl pkcs12 -export -clcerts \
-in /tmp/tsunami_test_server.crt \
-inkey /tmp/tsunami_test_server.key \
-out tsunami_test_server.p12
```
================================================
FILE: core.Dockerfile
================================================
# Stage 1: Build phase
FROM ghcr.io/google/tsunami-scanner-devel:latest AS build
## build the core engine
WORKDIR /usr/repos/tsunami-security-scanner
COPY . .
RUN mkdir -p /usr/tsunami
RUN gradle shadowJar
RUN find . -name 'tsunami-main-*.jar' -exec cp {} /usr/tsunami/tsunami.jar \;
RUN cp ./tsunami_tcs.yaml /usr/tsunami/tsunami.yaml
RUN cp plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml /usr/tsunami/payload_definitions.yaml
RUN cp -r plugin_server/py/ /usr/tsunami/py_server
## We perform a hotpatch of the path pointing to the payload definitions file
## for easier usage in the Dockerized environment.
RUN sed -i "s%'../../plugin/src/main/resources/com/google/tsunami/plugin/payload/payload_definitions.yaml'%'/usr/tsunami/payload_definitions.yaml'%g" \
/usr/tsunami/py_server/plugin/payload/payload_utility.py
## generate the protos for Python plugins
WORKDIR /usr/repos/tsunami-security-scanner/
RUN python3 -m grpc_tools.protoc \
-I/usr/repos/tsunami-security-scanner/proto \
--python_out=/usr/tsunami/py_server/ \
--grpc_python_out=/usr/tsunami/py_server/ \
/usr/repos/tsunami-security-scanner/proto/*.proto
# Stage 2: Release
FROM scratch AS release
COPY --from=build /usr/tsunami/tsunami.jar /usr/tsunami/
COPY --from=build /usr/tsunami/tsunami.yaml /usr/tsunami/
COPY --from=build /usr/tsunami/payload_definitions.yaml /usr/tsunami/payload_definitions.yaml
# Python server and the virtual environment
COPY --from=build /usr/tsunami/py_venv/ /usr/tsunami/py_venv
COPY --from=build /usr/tsunami/py_server/ /usr/tsunami/py_server
================================================
FILE: devel.Dockerfile
================================================
FROM ubuntu:latest
RUN apt-get update \
&& apt-get install -y --no-install-recommends git openjdk-21-jdk ca-certificates wget unzip python3 python3-venv \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /usr/share/doc && rm -rf /usr/share/man \
&& apt-get clean
# Install a specific version of protoc for the templated plugins
WORKDIR /usr/dependencies
RUN mkdir /usr/dependencies/protoc \
&& wget https://github.com/protocolbuffers/protobuf/releases/download/v25.5/protoc-25.5-linux-x86_64.zip -O /usr/dependencies/protoc.zip \
&& unzip /usr/dependencies/protoc.zip -d /usr/dependencies/protoc/
ENV PATH="${PATH}:/usr/dependencies/protoc/bin"
# Install a specific version of Gradle
WORKDIR /usr/dependencies
RUN wget https://services.gradle.org/distributions/gradle-8.14.2-bin.zip -O /usr/dependencies/gradle.zip \
&& unzip /usr/dependencies/gradle.zip -d /usr/dependencies/ \
&& mv /usr/dependencies/gradle-8.14.2/ /usr/dependencies/gradle/
ENV PATH="${PATH}:/usr/dependencies/gradle/bin"
# Prepare the virtualenv for Python plugins
# This is one of the few dependencies that will get carried to the final docker
# images.
WORKDIR /usr/tsunami/py_venv/
COPY plugin_server/py/requirements.in /usr/tsunami/py_venv/requirements.in
COPY plugin_server/py/requirements.txt /usr/tsunami/py_venv/requirements.txt
RUN python3 -m venv /usr/tsunami/py_venv
ENV PATH="/usr/tsunami/py_venv/bin:${PATH}"
RUN pip install --require-hashes -r /usr/tsunami/py_venv/requirements.txt
================================================
FILE: docs/_config.yml
================================================
remote_theme: pages-themes/cayman@v0.2.0
url: https://google.github.io
baseurl: /tsunami-security-scanner
paginate: 5
paginate_path: "/blog/page:num/"
plugins:
- jekyll-remote-theme
- jekyll-paginate
================================================
FILE: docs/_data/nav.yml
================================================
- title: "What's new"
path: /
- title: "All articles"
path: /blog/
- title: "Documentation"
path: /howto/
================================================
FILE: docs/_includes/nav.html
================================================
{% for nav in site.data.nav %}
{% if nav.subcategories != null %}
{% for subcategory in nav.subcategories %}
{{ subcategory.title }}
{% endfor %}
{% elsif nav.title == page.title %}
{{ nav.title }}
{% else %}
{{ nav.title }}
{% endif %}
{% endfor %}
================================================
FILE: docs/_layouts/default.html
================================================
{% seo %}
{% include head-custom.html %}
Skip to the content.
Posted on {{ page.date | date_to_long_string: "ordinal" }} by
{% for author in page.authors %}
{{ author.name }}
{% endfor %}
{{ content }}
================================================
FILE: docs/_posts/2024-03-19-tsunami-network-scanner-ai-security.md
================================================
---
authors:
- name: Annie Mao
excerpt: 'Interested in creating an AI-related plugin for the Tsunami network scanner and
getting rewarded for your efforts? See this post for details!'
title: 'Tsunami Network Scanner & AI Security'
---
You may already be familiar with the
[Tsunami Network Scanner](https://github.com/google/tsunami-security-scanner)
from our
[Patch Rewards program](https://bughunters.google.com/about/rules/4928084514701312/patch-rewards-program-rules#tsunami-patch-rewards),
which rewards external contributors for creating new
[detector plugins](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google).
Now with AI being on everyone's minds, we want to double down on securing open
source AI infrastructure via Tsunami.
On our
[GitHub page](https://github.com/google/tsunami-security-scanner-plugins/issues),
you can find a list of AI-relevant **plugin & web fingerprint** implementation
requests tagged as "help wanted". **Anyone** can contribute to a Tsunami plugin
from this list, and the implementation will be reviewed & rewarded under our
Tsunami Patch Rewards program, with rewards ranging from $500 to $3,133.7
([details](https://bughunters.google.com/about/rules/4928084514701312/patch-rewards-program-rules#reward-amounts-tsunami-)).
Here are the rules of engagement for implementing AI-related plugins:
* **First come, first served**: Each contributor can pick up any of the
unassigned plugins, but please only take one **at a time**.
* **Reassignment of inactive plugins**: If an assigned plugin has not been
worked on for **over a week**, then the Tsunami review panel will unassign
the contributor from the plugin. The plugin request is returned to the
free-for-all pool.
* **Vulnerability Research**: As a first step, the contributor has to provide
detailed vulnerability research & an implementation design for the plugin to
the review panel, and then wait for confirmation from the review panel
before moving on to the implementation stage.
* **Testbed Requirement**: All test containers or configurations for each
plugin have to be submitted to
[google/security-testbeds](https://github.com/google/security-testbeds).
* **Review Priority**: If a contributor already has a different plugin in the
review queue, we will prioritize reviewing the ML plugin, unless the
originally provided plugin is critical.
Finally, we welcome you to propose new plugins that address critical security
issues in AI-serving frameworks and related tools on our
[GitHub page](https://github.com/google/tsunami-security-scanner-plugins/issues).
For faster acceptance, when sharing your proposal, please provide context on how
a given service is used in the AI ecosystem.
We're looking forward to collaborating with you to keep AI infrastructure
secure!
================================================
FILE: docs/_posts/2025-06-18-changes-to-tsunami.md
================================================
---
authors:
- name: Pierre Precourt
excerpt: ‘Templated plugins are now the default for writing plugins and making the reward program more efficient.'
title: 'Changes to Tsunami'
---
# Changes to Tsunami
## Improving the situation on the Patch Reward Program (PRP)
Whether you have been a long time contributor or a newcomer to Tsunami, you
might have noticed that it takes a long time before a contribution is merged. We
thought it might be valuable to provide some context into why this happens:
- First and foremost, the Tsunami team is a rather small team (1-2 people)
with varying priorities.
- Even though we are working with partners to reduce the time to review
contributions, it still takes us a lot of time to merge contributions. That
is because the external version of Tsunami and the one we are using
internally are slightly different, most notably their build systems.
Now, this does not mean that we should not strive to make the situation better.
One bet that we are taking is templated plugins (introduced later in this
article). Without going into details, this new type of plugin should abstract
the differences between the two build systems, in a way that should make merging
plugins much easier for us and, thus, much faster for contributors.
## Tsunami templated plugins
To cite
[our official documentation](https://google.github.io/tsunami-security-scanner/howto/new-detector/templated/00-getting-started):
> In the past, if you wanted to write a Tsunami detector, you would need to
> implement your detector using Java or Python. For each, you would have to
> write a set of tests and ensure that everything is compiling and working as
> intended.
> This process proved to be very time consuming; especially as most Tsunami
> detectors are simply sending an HTTP request and checking the response code
> and body content. That is why we introduced templated plugins.
> We have abstracted most of the code required to write a plugin. All you need
> to do is to write a .textproto file that describes the behavior of your plugin
> and a _test.textproto file that describes the tests for the plugin.
In short, templated plugins allow contributors to write Tsunami plugins as if
they were configuration files.
### Why this change?
First and foremost, this makes writing Tsunami plugins much easier. It reduces
adherence to the build system and to the language, which makes it accessible to
more contributors. Because the plugins are now in a structured format, we can
also perform analysis on detectors and find common mistakes in these detectors:
this should overall improve the quality of plugins.
But that is only from the contributors’ perspective. From a maintainer
perspective, that also means that we can work more efficiently on changes at
scale in the Tsunami engine. Currently, whenever we need to make a change to the
engine, we often have to change about 100 plugins.
Finally, we are having very active discussions internally to rewrite Tsunami
entirely in Golang. If we were to decide to take this path, templated plugins
would help us make the migration easier.
### Why not YAML?
The most frequently asked question about templated plugins is: Why not use YAML
instead of textproto? First, we believe that
[YAML has a lot of issues](https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell).
But the most important reason is that textprotos are checked at compilation time
and enforce a strong structure to our plugins (on top of being smaller).
### How does this affect the Patch Reward Program (PRP)?
Templated plugins are now the default for writing plugins. **We will stop
accepting Java and Python plugins unless there is a good reason for it (we
understand that templated plugins can be limiting in some use cases)**.
Other than that, no big changes. The rewards will stay the same and so will the
queue system. If our bet shows to be successful and templated plugins really
reduce the time for plugins to be merged, we plan to increase the contributor
queue as well. This would mean that a contributor could work on more plugins in
parallel. Stay tuned.
### Will older plugins be rewritten?
Most likely. We have not yet come-up with a detailed plan on how to do it, but
we would like to rewrite as many plugins as possible to unify them.
## Tsunami releases
## Tsunami version 1.0.0
For a long time, Tsunami has been in Alpha release. Internally, we have been
using Tsunami consistently and at scale for a while now. Thus we believe that
Tsunami is ready to be officially released in version 1.0.0.
### Maven releases
For now, we are releasing most of Tsunami’s library to maven on repo central. If
you are depending on these artifacts, you will soon have to migrate away. We are
planning to change the way we are publishing Tsunami’s dependencies. The plan is
not finalized yet, but most likely we will publish Tsunami directly on GitHub.
================================================
FILE: docs/_posts/2025-10-16-october-update-tsunami-prp.md
================================================
# October update - Tsunami reward program
## Improving the PRP situation
Since our
[last update in June](https://google.github.io/tsunami-security-scanner/2025/06/18/changes-to-tsunami.html),
we have made good progress on merging incoming pull requests. Not only do we now
have a very low amount of requests to process, but most of them are now
implemented with the
[new templated language system](https://google.github.io/tsunami-security-scanner/howto/new-detector/templated/00-getting-started)
which is usually faster for us to merge.
**A big thank you to all of our contributors for their patience\!**
## An update on the payouts
Note:
[Our official rules](https://bughunters.google.com/about/rules/open-source/5067456626688000/tsunami-patch-rewards-program-rules)
have been updated accordingly.
We recently came to realize that our current payout system made the decision for
the reward difficult. To ensure everyone is rewarded fairly and adequately, we
have decided to simplify the payout system:
Type of detector | Reward (up to dollars)
:--------------------------------------------------: | :--------------------:
Wishlist detector | 3177.13
Exposed interface detector Weak credentials detector | 2000
Other detectors | 1500
### What is a wishlist detector?
This is a detector for a vulnerability that Google cares deeply about. We
understand that this is outside of the control of the contributors but this is
generally based on internal priorities.
We will generally make it explicit that a contribution falls in that category
but on the other hand, we might request that the detector is completed in a
faster timeline (less than a week) to justify the higher payout. Sometimes we
will release a wishlist to the public – if you pick up an item from that
wishlist, you are guaranteed to fall into this category.
### What happened to fingerprints?
We are not accepting new fingerprinting contributions for now. **Note that pull
requests already opened will be processed and paid as previously agreed upon.**
We are currently working on completely changing the way Tsunami performs
fingerprinting. Amongst other things, we are experimenting with rewriting that
specific portion of the scanner in Golang to measure how well the language
matches our needs.
## An insight into our triage decisions
We also understand that it might be difficult to understand how and why we
decide to accept some contributions and not others, so we wanted to provide some
visibility into that process.
First and foremost, the goal of Tsunami is to find impactful vulnerabilities.
**This generally means that we want to identify security issues that have a
strong impact; this generally translates to remote code execution (RCE).**
**The questions that we are always asking ourselves:**
* Can this be turned into a full-chain to remote code execution?
* Can the full-chain be implemented in the detector? Or be reliable enough
that it can ascertain the full chain exploitability?
Here is an example table for common vulnerability types:
| Category | Decision |
| :----------------------: | :-------------: |
| XSS | Rejected |
| CSRF | Rejected |
| SSRF | Likely rejected |
| SQLi | Likely rejected |
| Local file include | It depends |
| Path traversal | It depends |
| XXE | It depends |
| Remote file include | Likely accepted |
| File upload | Likely accepted |
| Exposed interface | Likely accepted |
| Authentication bypass | Likely accepted |
| Weak credentials | Likely accepted |
| OS command injection | Likely accepted |
As mentioned before, that decision depends heavily on the ability to create a
full chain of exploitation that leads to remote code execution.
## Tsunami versioning
As we previously announced, we are slowly dropping Maven releases in favor of
our Docker images and direct dependencies to GitHub. We are already not
publishing any new artifacts to Maven and encourage you **strongly** to migrate
to building with the GitHub code.
This change slightly increases overall maintenance of plugins for larger changes
of the core but ensures that issues do not go unnoticed and also makes
dependencies management a lot easier for us.
================================================
FILE: docs/about/index.md
================================================
# About Tsunami
## Why Tsunami?
When security vulnerabilities or misconfigurations are actively exploited by
attackers, organizations need to react quickly in order to protect potentially
vulnerable assets. As attackers increasingly invest in automation, the time
window to react to a newly released, high severity vulnerability is usually
measured in hours. This poses a significant challenge for large organizations
with thousands or even millions of internet-connected systems. In such
hyperscale environments, security vulnerabilities must be detected and ideally
remediated in a fully automated fashion. To do so, information security teams
need to have the ability to implement and roll out detectors for novel security
issues at scale in a very short amount of time. Furthermore, it is important
that the detection quality is consistently very high. To solve these challenges,
we created Tsunami - an extensible network scanning engine for detecting high
severity vulnerabilities with high confidence in an unauthenticated manner.
## Goals and Philosophy
* Tsunami supports small manually curated set of vulnerabilities
* Tsunami detects high severity, RCE-like vulnerabilities, which often
actively exploited in the wild
* Tsunami generates scan results with high confidence and minimal
false-positive rate.
* Tsunami detectors are easy to implement.
* Tsunami is easy to scale, executes fast and scans non-intrusively.
## Naming
The name "Tsunami" comes from the fact that this scanner is meant be used as part of a larger system to warn owners about automated "attack waves". Automated attacks are similar to tsunamis in the way that they come suddenly, without prior warning and can cause a lot of damage to organizations if no precautions are taken. The term "Tsunami Early Warning System Security Scanning Engine" is quite long and thus the name got abbreviated to Tsunami Scanning Engine, or Tsunami. Hence, the name is not an analogy to tsunamis itself, but to a system that detects them and warns everyone about them.
================================================
FILE: docs/assets/css/style.scss
================================================
---
---
@import '{{ site.theme }}';
.pagination {
text-align: center;
background-color: #eee;
border-radius: 0.3rem;
padding: 3px;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
================================================
FILE: docs/blog/index.html
================================================
---
title: Posts
layout: default
---
{% for post in paginator.posts %}
Posted on {{ post.date | date_to_long_string: "ordinal" }}
{{ post.excerpt }}
{% endfor %}
{% if paginator.previous_page %}
Previous
::
{% endif %}
{{ paginator.page }} of {{ paginator.total_pages }}
{% if paginator.next_page %}
:: Next
{% endif %}
================================================
FILE: docs/contribute/code-of-conduct.md
================================================
# Google Open Source Community Guidelines
At Google, we recognize and celebrate the creativity and collaboration of open
source contributors and the diversity of skills, experiences, cultures, and
opinions they bring to the projects and communities they participate in.
Every one of Google's open source projects and communities are inclusive
environments, based on treating all individuals respectfully, regardless of
gender identity and expression, sexual orientation, disabilities,
neurodiversity, physical appearance, body size, ethnicity, nationality, race,
age, religion, or similar personal characteristic.
We value diverse opinions, but we value respectful behavior more.
Respectful behavior includes:
* Being considerate, kind, constructive, and helpful.
* Not engaging in demeaning, discriminatory, harassing, hateful, sexualized,
or physically threatening behavior, speech, and imagery.
* Not engaging in unwanted physical contact.
Some Google open source projects [may adopt][] an explicit project code of
conduct, which may have additional detailed expectations for participants. Most
of those projects will use our [modified Contributor Covenant][].
[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct
[modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/
## Resolve peacefully
We do not believe that all conflict is necessarily bad; healthy debate and
disagreement often yields positive results. However, it is never okay to be
disrespectful.
If you see someone behaving disrespectfully, you are encouraged to address the
behavior directly with those involved. Many issues can be resolved quickly and
easily, and this gives people more control over the outcome of their dispute. If
you are unable to resolve the matter for any reason, or if the behavior is
threatening or harassing, report it. We are dedicated to providing an
environment where participants feel welcome and safe.
## Reporting problems
Some Google open source projects may adopt a project-specific code of conduct.
In those cases, a Google employee will be identified as the Project Steward, who
will receive and handle reports of code of conduct violations. In the event that
a project hasn’t identified a Project Steward, you can report problems by
emailing opensource@google.com.
We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is taken.
The identity of the reporter will be omitted from the details of the report
supplied to the accused. In potentially harmful situations, such as ongoing
harassment or threats to anyone's safety, we may take action without notice.
*This document was adapted from the [IndieWeb Code of Conduct][] and can also be
found at .*
[IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct
================================================
FILE: docs/contribute/contributing.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
================================================
FILE: docs/contribute/index.md
================================================
# Contributing to Tsunami
{% include_relative contributing.md %}
{% include_relative code-of-conduct.md %}
================================================
FILE: docs/howto/common-patterns.md
================================================
# Common detector patterns
### Running only for a specific service
*Use case: I want my detector to only run for web applications or for
application X.*
There exist currently two way in Tsunami to filter the service type:
1. Using annotations (preferred)
The
[`@ForWebService`](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/main/java/com/google/tsunami/plugin/annotations/ForWebService.java)
and
[`@ForServiceName({"X", "Y"})`](https://github.com/google/tsunami-security-scanner/blob/master/plugin/src/main/java/com/google/tsunami/plugin/annotations/ForServiceName.java)
annotations can be used to instruct the core engine of Tsunami to only run this
plugin if the service was a web service or a service with name `X` or `Y`. The
name of the service is obtained during the discovery phase. It currently is the
exact same service name as NMAP would report (e.g. `http`, `https`, `ssh`).
2. Using filtering (web service only)
The
[`NetworkServiceUtils.isWebService()`](https://github.com/google/tsunami-security-scanner/blob/483f9ea5b7c69e8802353e0dcd293c2f35eaa4aa/common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java#L69)
can be used when performing filtering to ensure only `NetworkService` that were
identified as web service will be processed.
Example usage:
```java
someNetworkServiceCollection.stream()
.filter(NetworkServiceUtils::isWebService)
// {...}
```
### Building URLs
*Use case: My detector targets a web application. How do I build the URL?*
When writing your plugins, there are a few things that you should NOT have to
care about and that the Tsunami core engine should resolve for you:
- Is the service using HTTP or HTTPS?
- How do I construct the URL from the `NetworkService`?
- What if NMAP fails to identify the service as HTTP and I still want to build
an URL?
All of these concerns are addressed in the core engine of Tsunami and you can
simply use the URL building API:
[`NetworkServiceUtils.buildWebApplicationRootUrl()`](https://github.com/google/tsunami-security-scanner/blob/483f9ea5b7c69e8802353e0dcd293c2f35eaa4aa/common/src/main/java/com/google/tsunami/common/data/NetworkServiceUtils.java#L173)
#### DO
```java
String myUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService)
+ MY_VULNERABLE_ENDPOINT;
```
#### DO NOT
The following **SHOULD NOT BE USED**:
1. Defining a `buildTarget` intermediate function is redundant and most of the
time not necessary:
```java
private static StringBuilder buildTarget(NetworkService networkService) {
StringBuilder targetUrlBuilder = new StringBuilder();
if (NetworkServiceUtils.isWebService(networkService)) {
targetUrlBuilder.append(NetworkServiceUtils.buildWebApplicationRootUrl(networkService));
} else {
targetUrlBuilder
.append("http://")
.append(toUriAuthority(networkService.getNetworkEndpoint()))
.append("/");
}
targetUrlBuilder.append(MY_VULNERABLE_ENDPOINT);
return targetUrlBuilder;
}
```
2. Using `String.Format` does not make use of the information obtained during
the discovery phase and is error prone:
```java
var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
var loginPageUrl = String.format("http://%s/%s", uriAuthority, MY_VULNERABLE_ENDPOINT);
```
### Adding command line arguments consumed by the detector
*Use case: I need command line arguments for my detector*
Tsunami uses [jCommander](https://jcommander.org/) for command line argument
parsing. In order to add new CLI arguments for your plugin, first define the
data class for holding all the arguments. You can follow the jCommander tutorial
to learn more about this tool.
For example:
```java
@Parameters(separators = "=")
public final class MyPluginArgs implements CliOption {
@Parameter(names = "--param", description = "Description for param.")
public String param;
@Override
public void validate() {
// Validate the command line value.
}
}
```
Then, the CLI flags will be parsed and an instance of this class will be created
by the main scanner at start-up time. In order to use this class in your plugin,
you can directly inject this data class into your plugin's constructor.
```java
public final class MyVulnDetector implements VulnDetector {
private final MyPluginArgs args;
@Inject
MyVulnDetector(MyPluginArgs args) {
this.args = checkNotNull(args);
}
// {...}
}
```
### Adding configuration properties consumed by the detector
*Use case: How do I add configurable option for my detector?*
Tsunami supports loading configs from a YAML file and uses
[snakeyaml](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation) to parse
the YAML config files. In order to add configuration properties to your plugin,
first you need to define a data class for holding all the configuration values.
NOTE: Currently Tsunami only supports standard Java data types for
configurations like `String`, numbers (`int`, `long`, `float`, `double`, etc),
`List` and `Map`.
```java
// All config classes must be annotated by this ConfigProperties annotation in
// order for Tsunami to recognize the config class.
@ConfigProperties(prefix = "my.plugin.configs")
public class MyPluginConfigs {
String stringValue;
long longValue;
List listValues;
Map mapValues;
}
```
Then, similar to the command line arguments, you can inject an instance of this
data class into your plugin's constructor.
```java
public final class MyVulnDetector implements VulnDetector {
private final MyPluginConfigs configs;
@Inject
MyVulnDetector(MyPluginConfigs configs) {
this.configs = checkNotNull(configs);
}
// {...}
}
```
The scanner will parse the configuration file when it starts, create an instance
of the data class from the config data, and inject the instance into your
plugin.
Following is an example config file for the previously defined `MyPluginConfigs`
object.
```yaml
my:
plugin:
configs:
# Config name can be exact match.
stringValue: "example value"
# Or matching via snake_case.
long_value: 123
list_values:
- "a"
- "b"
- "c"
mapValues:
key1: "value1"
key2: "value2"
```
To use the configuration file, you need to set the `tsunami.config.location`
option when calling Tsunami.
### Creating TCP sockets with proper timeouts
*Use case: My detector needs to create raw TCP or SSL sockets to communicate
with a service.*
When writing detectors that need to create raw TCP or SSL sockets, you **must**
use the `TsunamiSocketFactory` API instead of directly creating sockets through
`javax.net.SocketFactory` or `javax.net.ssl.SSLSocketFactory`. This ensures that
all sockets have proper timeout configurations, preventing plugins from hanging
indefinitely when servers don't respond.
#### Injecting the socket factory
```java
public final class MyVulnDetector implements VulnDetector {
private final TsunamiSocketFactory socketFactory;
@Inject
MyVulnDetector(TsunamiSocketFactory socketFactory) {
this.socketFactory = checkNotNull(socketFactory);
}
// {...}
}
```
#### Creating a plain TCP socket with default timeouts
```java
private final TsunamiSocketFactory socketFactory;
// Socket will have connect timeout of 10s and read timeout of 30s (configurable)
Socket socket = socketFactory.createSocket("example.com", 80);
try {
// Use the socket...
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
// ...
} finally {
socket.close();
}
```
#### Creating a socket with custom timeouts
```java
Socket socket = socketFactory.createSocket(
"example.com",
80,
Duration.ofSeconds(5), // Connect timeout
Duration.ofSeconds(15) // Read timeout
);
```
#### Creating an SSL/TLS socket
```java
// Create an SSL socket with default timeouts
SSLSocket sslSocket = socketFactory.createSslSocket("secure.example.com", 443);
// Or with custom timeouts
SSLSocket sslSocket = socketFactory.createSslSocket(
"secure.example.com",
443,
Duration.ofSeconds(5),
Duration.ofSeconds(15)
);
```
#### Upgrading a plain socket to SSL (STARTTLS)
```java
// First, create a plain socket
Socket plainSocket = socketFactory.createSocket("mail.example.com", 25);
// Send STARTTLS command...
// ...
// Then upgrade to SSL
SSLSocket sslSocket = socketFactory.wrapWithSsl(
plainSocket,
"mail.example.com",
25,
true // autoClose
);
```
#### Configuring socket timeouts
Socket timeouts can be configured via:
1. **Configuration file** (tsunami.yaml):
```yaml
common:
net:
socket:
connect_timeout_seconds: 10
read_timeout_seconds: 30
trust_all_certificates: true
```
1. **Command line options**:
```bash
--socket-connect-timeout-seconds=10
--socket-read-timeout-seconds=30
--socket-trust-all-certificates=true
```
CLI options take precedence over configuration file settings.
#### DO NOT create sockets directly
**NEVER** create sockets directly like this:
```java
// BAD: No timeout configured, socket may hang forever
Socket socket = new Socket("example.com", 80);
// BAD: Even with SocketFactory, timeout is missing
Socket socket = SocketFactory.getDefault().createSocket("example.com", 80);
```
Always use `TsunamiSocketFactory` to ensure proper timeout handling.
================================================
FILE: docs/howto/howto.md
================================================
# Build and run Tsunami
## Tsunami docker's environment
We provide a set of Docker images to help you build and use Tsunami. We provide
a minimal (scratch) image for:
- The core engine only;
- The callback server only;
- Each category of plugin;
Using these minimal images is not recommended, instead we recommend composing
on top of them.

## Running the latest version of Tsunami
If you just want to run the latest version of Tsunami, without having to
recompile anything, you can directly use the latest full image of Tsunami.
```sh
# Important: If you built a local version of the container, do not pull as it
# will overwrite your changes. Otherwise, do pull as you would be using a stale
# version of the image.
$ docker pull ghcr.io/google/tsunami-scanner-full
# Run the image
$ docker run -it --rm ghcr.io/google/tsunami-scanner-full bash
# If you want to use Python plugins
(docker) $ tsunami-py-server >/tmp/py_server.log 2>&1 &
# If you want to use the callback server
(docker) $ tsunami-tcs >/tmp/tcs_server.log 2>&1 &
# Run Tsunami
# Note: If you did not start the python server, omit the `--python-` arguments.
(docker) $ tsunami --ip-v4-target=127.0.0.1 --python-plugin-server-address=127.0.0.1 --python-plugin-server-port=34567
```
This images contains everything necessary under the `/usr/tsunami` directory.
To use the callback server, you might have to setup port forwarding with your
docker container when starting it. We encourage you to refer to the `-p` option
of Docker.
A few tips:
- Only scan one port: `--port-ranges-target`
- Only run your detector: `--detectors-include="detector-name"`; where detector
name is the name defined in `PluginInfo` section for Java and Python plugins and
the `info.name` section on templated plugins.
## Using docker to build Tsunami
In this section, we go through the different ways to compile the core engine
or a plugin locally so that you can test your changes.
It assumes that you have cloned both the `tsunami-security-scanner` and
`tsunami-security-scanner-plugins` repositories.
### Rebuilding the core engine
If you need to make changes to the core engine during the development cycle, you
will have to perform the following actions to test your change:
- Rebuild the core engine container;
```sh
# Build the core engine container
$ cd tsunami-security-scanner
$ docker build -t ghcr.io/google/tsunami-scanner-core:latest -f core.Dockerfile .
```
- Rebuild all plugins to ensure your change is compatible
IMPORTANT: Your changes must be committed via git to be picked. They do not need
to be pushed to GitHub, they can be local only.
In the following example, we will use docker volumes to mount our changes to
`/usr/tsunami/repos/tsunami-security-scanner`. This assumes that our
`tsunami-security-scanner` and `tsunami-security-scanner-plugins` clones are in
`/tsunami/` on our host. Also, our changes are committed to the `master` branch.
You can change the commands accordingly if your repositories path or branch are
different.
```sh
$ cd tsunami-security-scanner-plugins
```
First, we need to change the `Dockerfile` to use our changes:
```diff
-ENV GITREPO_TSUNAMI_CORE="https://github.com/google/tsunami-security-scanner.git"
-ENV GITBRANCH_TSUNAMI_CORE="stable"
+ENV GITREPO_TSUNAMI_CORE="/usr/tsunami/repos/tsunami-security-scanner"
+ENV GITBRANCH_TSUNAMI_CORE="master"
```
We also need to instruct docker to bind our changes in `/usr/tsunami/repos`:
```diff
- RUN gradle build
+ RUN --mount=type=bind,source=/tsunami-security-scanner,target=/usr/tsunami/repos/tsunami-security-scanner \
+ gradle build
+
```
Then we can rebuild all plugins in one swoop:
```sh
$ docker build -t ghcr.io/google/tsunami-plugins-all:latest --build-arg=TSUNAMI_PLUGIN_FOLDER=tsunami-security-scanner-plugins -f tsunami-security-scanner-plugins/Dockerfile /tsunami/
```
- Rebuild the `-full` container;
```sh
$ cd tsunami-security-scanner
```
We need to change the `full.Dockerfile` to use our newly created container:
```diff
# Plugins
- FROM ghcr.io/google/tsunami-plugins-google:latest AS plugins-google
- FROM ghcr.io/google/tsunami-plugins-templated:latest AS plugins-templated
- FROM ghcr.io/google/tsunami-plugins-doyensec:latest AS plugins-doyensec
- FROM ghcr.io/google/tsunami-plugins-community:latest AS plugins-community
- FROM ghcr.io/google/tsunami-plugins-govtech:latest AS plugins-govtech
- FROM ghcr.io/google/tsunami-plugins-facebook:latest AS plugins-facebook
- FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python
+ FROM ghcr.io/google/tsunami-plugins-all:latest AS plugins-all
{...}
- COPY --from=plugins-google /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-templated /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-doyensec /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-community /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-govtech /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-facebook /usr/tsunami/plugins/ /usr/tsunami/plugins/
- COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/
+ COPY --from=plugins-all /usr/tsunami/plugins/ /usr/tsunami/plugins/
```
And then rebuild it:
```sh
$ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile .
```
- Run the scanner to check that everything works.
See the "Running the latest version of Tsunami" section on this page to run
Tsunami with the newly built image. DO NOT perform a docker pull.
### Rebuilding a whole category of plugins
Tsunami groups plugins per categories. From the root folder of the plugin
repository, you can see that the categories are `google`, `community`,
`templated` and so on.
Our docker images are built separately for each category. The same Dockerfile
is used, but it is parameterized to use a different folder with
`TSUNAMI_PLUGIN_FOLDER`.
```sh
$ cd tsunami-security-scanner-plugins
$ build -t ghcr.io/google/tsunami-plugins-category:latest --build-arg TSUNAMI_PLUGIN_FOLDER=category .
# For example with the community category:
$ build -t ghcr.io/google/tsunami-plugins-community:latest --build-arg TSUNAMI_PLUGIN_FOLDER=community .
```
For **Python plugins**, you need to use the dedicated Dockerfile, which only
supports bundling all plugins:
```sh
$ cd tsunami-security-scanner-plugins
$ build -t ghcr.io/google/tsunami-plugins-python:latest -f python.Dockerfile .
```
Once you have rebuilt the categories that you need, you can rebuild the `-full`
image:
```sh
$ cd tsunami-security-scanner
$ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile .
```
Then follow "Running the latest version of Tsunami" to use this new image. DO
NOT perform a `docker pull`.
### Building an image for one plugin
Now, if during development you only wish to build your plugin, you can do so
by creating a new local-only category.
Before you start, you will need to change the definition of the
`full.Dockerfile` file:
- Add a `FROM` directive in the Plugins section:
```diff
FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python
+ FROM ghcr.io/google/tsunami-plugins-local:latest AS plugins-local
```
- Add a `COPY` directive in the section that copies everything:
```diff
COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/
+ COPY --from=plugins-local /usr/tsunami/plugins/ /usr/tsunami/plugins/
```
Then, you can build the actual image containing only your plugin:
```sh
$ cd tsunami-security-scanner-plugins
$ build -t ghcr.io/google/tsunami-plugins-local:latest --build-arg TSUNAMI_PLUGIN_FOLDER=path/to/my/plugin .
```
Finally, compile the `-full` image:
```sh
$ cd tsunami-security-scanner
$ docker build -t ghcr.io/google/tsunami-scanner-full:latest -f full.Dockerfile .
```
Then follow "Running the latest version of Tsunami" to use this new image. DO
NOT perform a `docker pull`.
**Python plugins** do not support building only one plugin. See building the
whole category instead.
## Building Tsunami without docker
We do not provide support for building Tsunami outside of our docker
environment.
You can use the Dockerfile provided in the repositories to build your own
toolchain if you so wish.
================================================
FILE: docs/howto/index.md
================================================
# Tsunami documentation
Welcome to the Tsunami community, we are thrilled that you want to contribute.
This page contains information to get you started with your first contributions.
## Contributing to Google code
- [Contributing rules]({{ site.baseurl }}/contribute/index)
## Understanding Tsunami
- [About Tsunami]({{ site.baseurl }}/about/index)
- [How tsunami works]({{ site.baseurl }}/howto/orchestration)
## Building and running Tsunami
- [How to build and run Tsumami]({{ site.baseurl }}/howto/howto)
## Writing plugins
### Vulnerability detectors
- [Using our custom configuration format]({{ site.baseurl }}/howto/new-detector/templated/00-getting-started) (preferred approach)
- [Using Java]({{ site.baseurl }}/howto/new-detector/new-detector-java)
- Using Python *(not documented yet)*
### Weak credentials detectors
Not documented yet.
### Fingerprinting plugins
Not documented yet.
## Other guides
- [Common detector patterns for Java plugins]({{ site.baseurl }}/howto/common-patterns)
(i.e. "How do I do that?!")
================================================
FILE: docs/howto/new-detector/new-detector-java.md
================================================
# Writing a Tsunami detector (Java)
NOTE: We now expect you to write plugins using the templated format first. Only
resort to Java or Python if the plugin cannot be written with the templated
format.
## Overview
Each Tsunami detector needs the following pieces which we will create in this
tutorial:
* A plugin name that is unique among all enabled Tsunami plugins.
* A set of build rules for [Gradle](https://gradle.org/) (external build)
* A `VulnDetector` that implements the vulnerability detection logic.
* A `PluginBootstrapModule` that provides necessary Guice bindings for the
detector.
* An optional `CliOption` that captures all the supported command line flags
for the detector.
* An optional `ConfigProperties` that captures all the supported configuration
for the detector.
## 1. Fork the examples
Tsunami provides a few example implementations of a `VulnDetector` plugin. The
examples live in the
[examples directory](https://github.com/google/tsunami-security-scanner-plugins/tree/master/examples)
* Update Java package names. The example `VulnDetector` plugin is defined
under `com.google.tsunami.plugins.example` package. Refactor the package and
class name according to your detector implementation.
* Give a meaningful description to the Gradle build rule at `build.gradle`. We
will work on the Gradle build rule later.
* Rewrite the `README.md` file to have a good explanation of your
`VulnDetector` plugin.
## 2. Putting the detector together
### 2.a - `PluginInfo` annotation
All Tsunami plugins must be annotated by the `PluginInfo` annotation. Otherwise
it cannot be identified by Tsunami scanner at runtime. This annotation provides
the general information about the plugin to the core scanner.
Following is an example usage of the `PluginInfo` annotation from our
`WordPressInstallPageDetector`.
```java
@PluginInfo(
// VULN_DETECTION PluginType is required for a VulnDetector plugin.
type = PluginType.VULN_DETECTION,
// This gives a human readable name for your VulnDetector. Can be different
// from your class name.
name = "WordPressInstallPageDetector",
// The current version of your plugin.
version = "0.1",
// A detailed description about what this plugin does.
description =
"This detector checks whether a WordPress install is unfinished. An unfinished WordPress"
+ " installation exposes the /wp-admin/install.php page, which allows attacker to set"
+ " the admin password and possibly compromise the system.",
// The author of this plugin.
author = "Tsunami Team (tsunami-dev@google.com)",
// The guice module that bootstraps this plugin.
bootstrapModule = WordPressInstallPageDetectorBootstrapModule.class)
```
### 2.b - Define the `VulnDetector` plugin
Each vulnerability detector plugin is an implementation of the `VulnDetector`
interface. For this step we only explain the placeholder code, later you'll need
to provide implementations for the class itself.
Following is an example placeholder code from the
`WordPressInstallPageDetector`:
```java
// ...
// annotations
// ...
public final class WordPressInstallPageDetector implements VulnDetector {
// See https://github.com/google/flogger for details.
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
// Tsunami uses Guice (https://github.com/google/guice) to manage the
// dependencies.
@Inject
WordPressInstallPageDetector(
// Tsunami provides a UtcClock for production and FakeUtcClock for
// testing purposes.
@UtcClock Clock utcClock,
// You can also inject other useful dependencies to your plugin code, e.g.
// inject HttpClient if you need to interact with a web server.
HttpClient httpClient) {
}
// The entrypoint of the VulnDetector. We will explain this later.
@Override
public DetectionReportList detect(
TargetInfo targetInfo, ImmutableList matchedServices) {
// implement me.
}
}
```
### 2.c - Implement the main detection logic
The main logic of the detection is expected to happen in the `detect` method,
which expects two arguments:
1. [The `TargetInfo` proto](https://github.com/google/tsunami-security-scanner/blob/master/proto/reconnaissance.proto).
This proto contains information that were gathered during the fingerprinting
and discovery phase.
1. [The `NetworkService` list](https://github.com/google/tsunami-security-scanner/blob/master/proto/network_service.proto).
This list contains all the identified network services that match the
service filtering annotations.
The main detection logic usually iterates over all the elements of the
`matchedServices` parameter and checks whether the `NetworkService` is
vulnerable to the vulnerability your plugin checks for. If any service is
vulnerable, you'll need to build a `DetectionReport` proto that explains the
identified vulnerability.
Following is an example implementation from our `WordPressInstallPageDetector`:
```java
@Override
public DetectionReportList detect(
TargetInfo targetInfo, ImmutableList matchedServices) {
return DetectionReportList.newBuilder()
.addAllDetectionReports(
matchedServices.stream()
// The WordPressInstallPageDetector only works for web services.
.filter(NetworkServiceUtils::isWebService)
// Detection logic that checks whether a web service exposes
// a wordpress installation page. Omitted here for simplicity.
.filter(this::isServiceVulnerable)
// Build a DetectionReport when the web service is vulnerable.
.map(networkService -> buildDetectionReport(targetInfo, networkService))
.collect(toImmutableList()))
.build();
}
private DetectionReport buildDetectionReport(
TargetInfo scannedTarget, NetworkService vulnerableNetworkService) {
return DetectionReport.newBuilder()
.setTargetInfo(scannedTarget)
.setNetworkService(vulnerableNetworkService)
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
.setVulnerability(
Vulnerability.newBuilder()
.setMainId(
VulnerabilityId.newBuilder()
.setPublisher("GOOGLE")
.setValue("UNFINISHED_WORD_PRESS_INSTALLATION"))
.setSeverity(Severity.CRITICAL)
.setTitle("Unfinished WordPress Installation")
.setDescription(
"An unfinished WordPress installation exposes the /wp-admin/install.php page,"
+ " which allows attacker to set the admin password and possibly"
+ " compromise the system."))
.build();
}
```
## 3. Preparing the `PluginBootstrapModule`
Each Tsunami plugin must have a companion `PluginBootstrapModule` that provides
the required Guice bindings and registers the plugin to the core engine.
Creating a `PluginBootstrampModule` is rather simple if you only need to
register the plugin. You only need to call `registerPlugin` within the
`configurePlugin` method (e.g.
[`ExampleVulnDetectorBootstrapModule`](https://github.com/google/tsunami-security-scanner-plugins/tree/master/examples/example_vuln_detector/src/main/java/com/google/tsunami/plugins/example/ExampleVulnDetectorBootstrapModule.java)).
A more complete example is the
[GenericWeakCredentialDetectorBootstrapModule](https://github.com/google/tsunami-security-scanner-plugins/blob/master/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java)
================================================
FILE: docs/howto/new-detector/templated/00-getting-started.md
================================================
# Getting started with templated plugins
In this documentation, you will learn how to write a plugin for Tsunami in the
templated format.
## What will be covered
- [Introduction](01-introduction)
* What is a templated plugin?
* How does it work?
* How do I know how to write a templated plugin?
* Execution workflow of a templated plugin
- [Bootstrapping the plugin](02-bootstrapping)
* Understanding the vulnerability
* Providing information about our plugin and the vulnerability
* Configuring the plugin
- [Writing the first actions](03-first-actions)
- [Putting it together in workflows](04-workflows)
- [Using variables](05-variables)
* Using variables
* Extracting information to local variables
* Predefined variables
- [Using the callback server](06-callback-server)
- [Using cleanup actions](07-cleanup-actions)
- [Writing unit tests](08-writing-unit-tests)
- Glossaries
* [Predefined variables](glossary-predefined-variables)
* [Magic tests URIs when mocking HTTP](glossary-tests-magic-uri)
- Appendixes
* [Convention: How to name a plugin](appendix-naming-plugin)
* [Convention: How to name an action](appendix-naming-actions)
* [Convention: Naming tests](appendix-naming-tests)
* [Using the linter](appendix-using-linter)
## Let's get started!
[Introduction](01-introduction)
================================================
FILE: docs/howto/new-detector/templated/01-introduction.md
================================================
# Introduction
## What is a templated plugin?
In the past, if you wanted to write a Tsunami detector, you would need to
implement your detector using Java or Python. For each, you would have to write
a set of tests and ensure that everything is compiling and working as intended.
This process proved to be very time consuming; especially as most Tsunami
detectors are simply sending an HTTP request and checking the response code and
body content. That is why we introduced templated plugins.
## How does it work?
We have abstracted most of the code required to write a plugin. All you need to
do is to write a `.textproto` file that describes the behavior of your
plugin and a `_test.textproto` file that describes the tests for the plugin.
A `.textproto` is a human-readable text representation of a protocol buffer
message. If you are not familiar with protocol buffers, we recommend checking
the [official documentation](https://protobuf.dev/), but for our use case, you
can think of it as a strongly typed JSON or YAML.
The `.textproto` files are compiled into binary format and embedded as resources
to a meta plugin that we will refer to as the templated Tsunami plugin. At
runtime, the templated plugin will interpret the behavior described in each file
and dynamically create a new detector for it.

## How do I know how to write a templated plugin?
We have tried to cover as much as possible in this documentation. But the
configuration language is bound to evolve with time. If you have any doubt, the
source of truth will always be the
[proto definition](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/proto)
which we aim at keeping as straightforward and commented as possible.
## Execution workflow of a templated plugin
Each plugin is defined by a set of two high-level concepts: **Actions** and
**workflows**.
- **Actions** are the basic unit of execution in a templated plugin. They are
responsible for performing one specific... well... action and returning a
boolean value that defines whether it was successful. An example action, in
plain English, could be:
> Send an HTTP request to the target and verify that the returned status code
> is 200
- Once you have defined a set of actions, you need to be able to define an order
in which they will be executed: this is the role of **workflows**.
To write a plugin, we need to define a set of actions and put them in a
specific order with workflows. But how are things executed? The engine processes
plugins in the following way:
1. Extracts all the workflows and actions defined in the plugin;
2. Performs very basic checks to ensure that everything is well defined;
3. Goes through all the defined workflows and execute the first that matches
the conditions it was defined with;
4. Executes every action of the workflow in order until one fails or all actions
are successful;
5. If any action failed, there is no vulnerability. If all actions were
successful, the vulnerability is present.
Steps 4 and 5 are repeated for every network service found during the port
scanning phase of Tsunami. We call steps 4 and 5 a **run of the workflow**.
## What is next
[Bootstrapping the plugin](02-bootstrapping)
================================================
FILE: docs/howto/new-detector/templated/02-bootstrapping.md
================================================
# Bootstrapping the plugin
*This section assumes that you already know how to
[build and run Tsunami](https://google.github.io/tsunami-security-scanner/howto/howto).*
## Before starting
Before we start, we encourage you to take a quick look at the setup guide for
the linter of the configuration language. Your plugin will be expected to pass
all the checks in this linter. Warnings will have to be justified before being
merged as well.
[See the instructions for the linter](appendix-using-linter)
## Our vulnerable application
For this tutorial series, we will write a simple detector for a non-existing
vulnerability `CVE-0123-12345`. We want our detector to:
- *Fingerprint the application*: we only want to send other requests if we are
sure that we are dealing with the potentially vulnerable application; So we are
going to send an HTTP request to `/version` and check for the presence of the
banner `MyVulnerableApp` in the `Server` header;
- *Exploit the application*: once we are sure that we are dealing with the right
application, we are going to send a `POST` request to `/exploit` with a custom
payload `%{ print("tsunami_%d_marker", 1250*1+3) }%` in the `process` field.
Then we are going to verify that it gets executed by verifying that the answer
contains `tsunami_1253_marker`;
*Note: In this example, we simulate a template injection vulnerability that is
going to cause `%{ print("tsunami_%d_marker", 1250*1+3) }%` to be interpreted.
The vulnerable language or the syntax have no importance. The result of
`1250*1+3` is going to be replaced at the `%d` placeholder, resulting in
`tsunami_1253_marker` being printed in the response.*
## Providing information about our plugin
Let's create a new file in our local copy of the
[Tsunami plugin repository](https://github.com/google/tsunami-security-scanner-plugins)
as `templated/templateddetector/plugins/cve/0123/MyVulnerableApp_CVE_0123_12345.textproto`.
Let's add the header that will help linters to understand which proto definition
we are using:
```proto
# proto-file: third_party/tsunami_plugins/templated/templateddetector/proto/templated_plugin.proto
# proto-message: TemplatedPlugin
```
Then we provide basic information about our plugin. Note that the name of the
plugin is used to **uniquely identify it and thus should be unique across all
plugins**.
```proto
info: {
type: VULN_DETECTION
name: "MyVulnerableApp_CVE_0123_12345"
author: "Some developper "
version: "1.0"
}
```
Now is a good time to read about our
[naming conventions for plugins](appendix-naming-plugin).
## Information about the vulnerability
Let's add information about the vulnerability itself. These are the information
that will be used to generate the vulnerability report and notify about the
vulnerability.
Important: whenever a CVE is associated with the vulnerability the `related_id`
field must be filled with the associated CVE reference.
```proto
finding: {
main_id: {
publisher: "GOOGLE"
value: "SOME_PLUGIN_DETECTION"
}
title: "Example templated plugin"
description: "This is an example templated plugin."
recommendation: "No recommendation, this is an example."
related_id: {
publisher: "CVE"
value: "CVE-0123-12345"
}
}
```
## Configuring the plugin
The next section in our file, is the `config` section. It allows some basic
tuning of the plugin. For now, let's keep it empty:
```proto
config: {}
```
But it is important to keep in mind that this section allows:
- To enable `debug` mode. In debug mode, the plugin will generate highly verbose
debugging messages. For example, it will log every HTTP request and response;
- To switch the plugin to be `disabled`. Note that even when disabled (unless
explicitly specified in the test file) the tests for that plugin will still be
executed.
If we were to run our plugin now, we would see the plugin being registered but
we would get a non-blocking error at runtime because we have no workflow or
action defined:
```sh
{ ... }
Dec 31, 2024 11:07:58 AM com.google.tsunami.plugin.PluginBootstrapModule registerDynamicPlugin
INFO: Dynamic plugin registered: MyVulnerableApp_CVE_0123_12345
{ ... }
Dec 31, 2024 11:08:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect
INFO: Starting detector: MyVulnerableApp_CVE_0123_12345
Dec 31, 2024 11:08:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect
SEVERE: No workflow matched the current setup. Is plugin 'MyVulnerableApp_CVE_0123_12345' misconfigured?
{ ... }
```
## What is next
[Writing the first actions](03-first-actions)
================================================
FILE: docs/howto/new-detector/templated/03-first-actions.md
================================================
# Writing the first actions
Each action is defined by a name and a subtype. A subtype defines what the
action is able to do: For example "HTTP request" is a subtype that allows to
send an HTTP request and inspect responses.
Let's start building our fingerprinting step with a very simple action:
```proto
actions: {
name: "fingerprinting"
http_request: {
method: GET
uri: "/version"
}
}
```
In that action, named `fingerprinting` we simply send a `GET` HTTP request to
`/version` on the currently targeted service.
*Note that Tsunami will only send HTTP requests to services that have been
identified as HTTP services during the port scanning phase.*
But that action will always succeed because it does not verify anything in the
response. Let's use the `response` field and add a condition on the status code:
```proto
actions: {
name: "fingerprinting"
http_request: {
method: GET
uri: "/version"
response: {
http_status: 200
}
}
}
```
Now we want to ensure the header contains `MyVulnerableApp`: we need to use
**expectations**. We can either use `expect_all` or `expect_any`, which perform
checks on the response that must respectively "all be true" or
"at least one true". Here, we have only one expectation, so we will use
`expect_all` with one `conditions`:
```proto
actions: {
name: "fingerprinting"
http_request: {
method: GET
uri: "/version"
response: {
http_status: 200
expect_all: {
conditions: [
{ header: { name: "Server" } contains: "MyVulnerableApp" }
]
}
}
}
}
```
The condition is that the `header` with `name` `Server` `contains` the string
`MyVulnerableApp`.
Now, let's build a `POST` request for our exploitation step:
```proto
actions: {
name: "exploitation"
http_request: {
method: POST
uri: "/exploit"
data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%"
response: {
http_status: 200
expect_all: {
conditions: [
{ body: {} contains: "tsunami_1253_marker" }
]
}
}
}
}
```
This action is very similar to the previous one but:
- It sends a `POST` request instead of a `GET` one;
- As part of the `POST` it sends
`process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%` as data;
- The expectation has been changed to check that the response body
contains `tsunami_1253_marker`.
## Redirects
Note that by default, the HTTP client will follow any HTTP redirects.
If you wish to change that behavior, you can configure your HTTP request using
the `client_options` stanza. For example, with our previous request:
```proto
actions: {
name: "exploitation"
http_request: {
method: POST
uri: "/exploit"
data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%"
client_options: {
disable_follow_redirects: true
}
response: {
http_status: 200
expect_all: {
conditions: [
{ body: {} contains: "tsunami_1253_marker" }
]
}
}
}
}
```
## What is next
[Putting it together in workflows](04-workflows)
================================================
FILE: docs/howto/new-detector/templated/04-workflows.md
================================================
# Assembling the workflow
A workflow is simply a list of the actions to be executed, in order:
```proto
workflows: {
actions: [
"fingerprinting",
"exploitation"
]
}
```
And that's it! We have written our first templated plugin. What happens if we
run it against a default HTTP server?
```sh
{ ... }
Dec 31, 2024 11:29:58 AM com.google.tsunami.plugin.PluginBootstrapModule registerDynamicPlugin
INFO: Dynamic plugin registered: MyVulnerableApp_CVE_0123_12345
{ ... }
Dec 31, 2024 11:30:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector detect
INFO: Starting detector: MyVulnerableApp_CVE_0123_12345
Dec 31, 2024 11:30:15 AM com.google.tsunami.common.net.http.OkHttpHttpClient send
INFO: Sending HTTP 'GET' request to 'http://127.0.0.1:9090/version'.
Dec 31, 2024 11:30:15 AM com.google.tsunami.common.net.http.OkHttpHttpClient parseResponse
INFO: Received HTTP response with code '404' for request to 'http://127.0.0.1:9090/version'.
Dec 31, 2024 11:30:15 AM com.google.tsunami.plugins.detectors.templateddetector.TemplatedDetector runWorkflowForService
INFO: No vulnerability found because action 'Fingerprint' failed.
Dec 31, 2024 11:30:15 AM com.google.tsunami.plugin.PluginExecutorImpl buildSucceededResult
INFO: MyVulnerableApp_CVE_0123_12345 plugin execution finished in 119 (ms)
{ ... }
```
As expected, a default HTTP server does not have a `Server` header that contains
`MyVulnerableApp`.
## What is next
[Using variables](05-variables)
================================================
FILE: docs/howto/new-detector/templated/05-variables.md
================================================
# Using variables
In our action, we have hardcoded our payload, which is not super convenient and
readable:
```proto
actions: {
name: "exploitation"
http_request: {
method: POST
uri: "/exploit"
data: "process=%{ print(\"tsunami_%d_marker\", 1250*1+3) }%"
response: {
http_status: 200
expect_all: {
conditions: [
{ body: {} contains: "tsunami_1253_marker" }
]
}
}
}
}
```
Let's try to move our payload into a variable. For this, we will define
variables in our workflow:
```proto
workflows: {
variables: [
{ name: "payload" value: "%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" },
{ name: "payload_result" value: "tsunami_1253_marker" }
]
actions: [
"fingerprinting",
"exploitation"
]
}
```
Which can then be used in the action:
{% raw %}
```proto
actions: {
name: "exploitation"
http_request: {
method: POST
uri: "/exploit"
data: "process={{ payload }}"
response: {
http_status: 200
expect_all: {
conditions: [
{ body: {} contains: "{{ payload_result }}" }
]
}
}
}
}
```
{% endraw %}
IMPORTANT: The syntax for variables is requires **exactly** one space before and
after the variable name, between the brackets. Otherwise, substitution will not
happen.
## Extracting information to local variables
Expectations are sufficient if we need to check that the response has a very
specific content. But what if we need to extract some information from the
response for later use? For example, what if we have an action that creates a
job and we need the job name in a follow-up action to trigger the vulnerability?
In that case we can use **extractions** and **local variables**. But what are
local variables? There are two types of variables:
- **Workflow variables**: Defined at the workflow level they mostly define
static content. They will exist for the whole lifecycle of the workflow and will
be reset to their defined value between workflow runs. Note that workflow
variables are technically mutable, but we strongly recommend not mutating
them (i.e. not using them in extractions). We used this type of variable in the
previous example.
- **Local variables**: These variables are more dynamic. They are defined from
**extractions** and are only valid for the current workflow run. They are mostly
used to extract information that will be used in a later action.
For our previous example, let us assume that the `/version` page returns a CSRF
token that we will need to use in the `/exploit` request. We can use a local
variable to store that value:
```proto
actions: {
name: "fingerprinting"
http_request: {
method: GET
uri: "/version"
response: {
http_status: 200
expect_all: {
conditions: [
{ header: { name: "Server" } contains: "MyVulnerableApp" }
]
}
extract_all: {
patterns: [
{
from_body: {}
regexp: "CSRFToken=([a-zA-Z0-9_]+)"
variable_name: "csrf_token"
},
]
}
}
}
}
```
In that example, we:
- still verify that the server has the `Server` header that we expect;
- also try to `extract_all` `patterns` `from_body` given regular expression
`regexp` and store the result in the variable `variable_name` (here
"csrf_token").
The extraction system offers **exactly** one capture group and extracting
several information should be set into different patterns.
WARNING: The use of extractions with `extract_any` should be done carefully. We
very strongly recommend to only use `extract_any` with the
**same variable name** between extractions. Doing otherwise makes the plugin
potentially flaky and difficult to debug.
## Predefined variables
Now would be a good time to look at the
[list of variables](glossary-predefined-variables) that Tsunami provides.
## What is next
[Using the callback server](06-callback-server)
================================================
FILE: docs/howto/new-detector/templated/06-callback-server.md
================================================
# Using the callback server
If you wrote plugins for Tsunami before, you know about the callback server. If
you have not, the callback server is a mechanism that allows us to check for
vulnerabilities via an out-of-band mechanism. You can read more about it on the
[dedicated GitHub repository](https://github.com/google/tsunami-security-scanner-callback-server).
In a nutshell, the callback server works this way:
1. A secret is generated (and stored in `T_CBS_SECRET`);
2. This secret is hashed;
3. The exploit uses the hashed secret and the URL of the callback server
(`T_CBS_URI`) to trigger an out-of-band communication with the callback server;
4. The secret can be used to ask the callback server if a communication using
the hashed secret has been logged (i.e. if the vulnerability has been
triggered);
Currently, the templated plugin system does not support payload generation,
so communication with the callback server has to be handled semi-manually.
Every workflow run will automatically generate a new secret, its hash and the
adequate trigger URL. That means that, as part of your exploit, you will need to
make sure a request to the callback server is made with the trigger URL. The
trigger URL is stored in the `T_CBS_URI` variable. For example, in our previous
example we could change the payload to:
{% raw %}
```proto
{ name: "payload" value: "%{ import os; os.system('curl {{ T_CBS_URI }}') }%" }
```
{% endraw %}
`T_CBS_URI` would be replaced with the trigger URL and the callback server
would receive a request on that endpoint if the vulnerability is triggered.
Checking if the vulnerability was triggered then simply requires defining a new
action:
```proto
actions: {
name: "check_callback_server_logs"
callback_server: { action_type: CHECK }
}
```
And, voila! But wait... How do we know, in our plugin, if the callback server
is currently running? Do we not want to support both use cases? That is one of
the features offered by workflows: `condition` allows you to define a condition
for the workflow to be executed. With our example:
{% raw %}
```proto
# Workflow that uses the callback server.
workflows: {
condition: REQUIRES_CALLBACK_SERVER
variables: [
{ name: "payload" value: "%{ import os; os.system('curl {{ T_CBS_URI }}') }%" }
## note: empty string is always present in the body. This cancels out the
## body content expectation.
{ name: "payload_result" value: "" }
]
actions: [
"fingerprinting",
"exploitation",
"check_callback_server_logs"
]
}
# Workflow that does not use the callback server.
workflows: {
variables: [
{ name: "payload" value: "%{ print(\"tsunami_%d_marker\", 1250*1+3) }%" },
{ name: "payload_result" value: "tsunami_1253_marker" }
]
actions: [
"fingerprinting",
"exploitation"
]
}
```
{% endraw %}
Note: Because workflow are interpreted in order, the one that is more
restrictive needs to be defined first. Otherwise, the less restrictive workflow
would always be the one running.
## What is next
[Using cleanup actions](07-cleanup-actions)
================================================
FILE: docs/howto/new-detector/templated/07-cleanup-actions.md
================================================
# Cleanup actions
In our example, none of the defined actions will modify the target. But what if
we had some actions that we want to clean-up after? Cleanup actions are the
solution.
Let us assume an arbitrary example that would cause a change on the target:
```proto
actions: {
name: "create_docker_container"
http_request: {
method: POST
uri: "/api/v1/create"
data: "name=MySuperContainer"
response: {
http_status: 200
}
}
}
```
In that example, if the request is successful, a new container will be created
on the target, but we want to make sure to delete that container afterwards.
Because cleanup actions are normal actions, we can start by writing the deletion
request:
```proto
actions: {
name: "cleanup_container"
http_request: {
method: POST
uri: "/api/v1/delete"
data: "name=MySuperContainer"
response: {
http_status: 200
}
}
}
```
This action will ensure the container is deleted. But now, how do we make sure
it is executed after and only if the container has been created? We register
it as a cleanup of the initial action:
```proto
actions: {
name: "create_docker_container"
http_request: {
method: POST
uri: "/api/v1/create"
data: "name=MySuperContainer"
response: {
http_status: 200
}
}
cleanup_actions: "cleanup_container"
}
```
Note the newly added `cleanup_actions` entry. This will ensure the following:
- If `create_docker_container` is not executed or fail, the cleanup action will
not be executed;
- If the `create_docker_container` is executed successfully, the cleanup action
will always be executed at the end of the current workflow run;
## What is next
[Writing unit tests](08-writing-unit-tests)
================================================
FILE: docs/howto/new-detector/templated/08-writing-unit-tests.md
================================================
# Writing unit tests
For every workflow, we expect to see associated unit tests for each of your
contributions. If unit tests are not as reliable as integration testing,
especially in the context of the scanner, they provide some insurance that
changes are not breaking detectors.
## Configuration of the unit test
The tests for a specific plugin are defined in a test file that is named exactly
like the detector, with the `_test` suffix. For example, for a
`NodeRED_ExposedUI.textproto` you can find its
`NodeRED_ExposedUI_test.textproto` counterpart.
Once created, the file needs to contain a minimal configuration:
```proto
config: {
tested_plugin: "nameOfTheTestedPlugin"
}
```
Where `nameOfTheTestedPlugin` is the name of the plugin. The `tested_plugin`
configuration indicates to the test engine which detector to bind the test to.
Without this option, your test will not work.
You can additionally define the `disabled` configuration option, but in most
cases you will not need to use it.
## Defining tests
Once you have configured the general option for your unit tests, you need to
actually define each tests.
Most test will rely on a mock server to simulate the target application that
we wrote a detector for. But before we dig deeper into mock servers, each
test needs to have a name and whether the vulnerability will be found, for
example:
```proto
tests: {
name: "whenVulnerable_returnsTrue"
expect_vulnerability: true
}
tests: {
name: "whenNotVulnerable_returnsFalse"
expect_vulnerability: false
}
```
See our [convention on naming tests](appendix-naming-tests).
## Using mock servers
Now we can start simulating the behavior of our vulnerable application. Two mock
capabilities are currently integrated in the templated plugin system:
- An HTTP server;
- A fake Callback server;
Several mocks can be used at the same time.
Let us take a simplified version of our previously defined plugin:
```proto
actions: {
name: "exploitation"
http_request: {
method: POST
uri: "/exploit"
data: "process=%{ import os; os.system('curl {{ T_CBS_URI }}') }%"
response: {
http_status: 200
}
}
}
actions: {
name: "check_callback_server_logs"
callback_server: { action_type: CHECK }
}
workflows: {
condition: REQUIRES_CALLBACK_SERVER
actions: [
"exploitation",
"check_callback_server_logs"
]
}
```
To validate vulnerability detection of this plugin, we will need:
- To simulate the callback server to return true. This can easily be done with
the `mock_callback_server` directive;
- To simulate the answer to `/exploit` with the HTTP server which can easily be
done with the `mock_http_server` directive;
```proto
tests: {
name: "whenVulnerable_returnsTrue"
expect_vulnerability: true
mock_callback_server: {
enabled: true
has_interaction: true
}
mock_http_server: {
mock_responses: [
{
uri: "/exploit"
status: 200
},
]
}
}
```
And that is it. If we wanted to check a case where the server is not vulnerable
we could use the following test case:
```proto
tests: {
name: "whenNotVulnerable_returnFalse"
expect_vulnerability: false
mock_http_server: {
mock_responses: [
{
uri: "/exploit"
status: 403
},
]
}
}
```
Note that we do not need the callback server anymore as the workflow will fail
before. Additionally, the `/exploit` endpoint now returns a `403`.
## Adding a bit of magic to our world
When mocking HTTP responses, Tsunami provides a few magic endpoints (to be
used in the `uri` field).
You can view them in the [glossary](glossary-tests-magic-uri).
## Generating things for you
Finally, under the hood, Tsunami will also generate unit tests for you. This
helps us detecting flaky detectors: for example when a detector fails to pass
the "echo server" test (when the server is just repeating the request, a
detector should not raise a vulnerability).
## Congratulations!
Congratulations, you have finished writing your first templated plugin!
================================================
FILE: docs/howto/new-detector/templated/appendix-naming-actions.md
================================================
# Convention: How to name actions
Actions must be named using the `[a-zA-Z0-9_]` character set. For example
`this_is_my_action`. This naming convention helps improving discoverability of
actions.
================================================
FILE: docs/howto/new-detector/templated/appendix-naming-plugin.md
================================================
# Convention: How to name a plugin
The plugin name and filename should be identical as it makes for easier
discoverability. Plugins should be named using the following convention:
- All plugins should be named using the following character set: `[a-zA-Z0-9_]`
so no spaces or special characters.
- If the vulnerability has an associated CVE:
`VulnerableApplicationName_CVE_YYYY_NNNNN` and the plugin should be placed in
the `cve/YYYY/` directory.
- If the vulnerability does not have an associated CVE:
`VulnerableApplicationName_YYYY_VulnerabilityName`; if a vulnerability has no
name you can try to describe it, for example `PreauthRCE`. The vulnerability
will then be placed in the directory that matches the type of vulnerability,
for example `rce/YYYY/VulnerableApplicationName_YYYY_VulnerabilityName`.
- When the name of a plugin contains an acronym (e.g. `HTTP`, `UI`, `RCE`),
that acronym must be in uppercase.
================================================
FILE: docs/howto/new-detector/templated/appendix-naming-tests.md
================================================
# How to name unit tests
Use `condition_outcome` as the naming schema for your tests. That explicitly
means that you should not prefix the test function name with "test".
Example: `whenVulnerable_returnsTrue`.
================================================
FILE: docs/howto/new-detector/templated/appendix-using-linter.md
================================================
# Using the linter
For all plugins written using our configuration format, we expect the plugins to
be linted.
The linter ensures that the plugin has the right format but also performs a
series of checks that make sure it is behaving correctly.
## Installing the linter
### Using our docker image
The linter is bundled in our docker image and will automatically run.
### Custom setup
The linter is a Go binary that can very easily be installed:
```
$ go install github.com/google/tsunami-security-scanner-plugins/templated/utils/linter@latest
$ linter
```
Note that depending on your current configuration, you might have to extend your
`PATH`. See the
[Golang documentation](https://go.dev/doc/tutorial/compile-install) for details.
================================================
FILE: docs/howto/new-detector/templated/glossary-predefined-variables.md
================================================
# Predefined variables
Tsunami will provide a predefined set of variable to the environment that you
can make use of in your actions. We try to maintain a strong naming convention
for these :
- `T_` stands for Tsunami and identifies a variable that is provided by the
core engine;
- `_UTL_` stands for utility and provides various utility variables;
- `_NS_` stands for network service and provide information about the currently
scanned network service;
- `_CBS_` stands for callback server and provides information about the
callback server.
Here is the list of variables that are provided:
- `T_UTL_CURRENT_TIMESTAMP_MS`: Provides the current timestamp in milliseconds.
Note that the timestamp is computed at the beginning of a workflow run. It will
thus be different between services but always return the same value within one
run;
- `T_NS_BASEURL`: The base URL of the network service being scanned. For example
`http://127.0.0.1:9090` or `http://hostname.lan:1000`;
- `T_NS_PROTOCOL`: The protocol used by the network service being scanned (e.g.
`tcp`);
- `T_NS_HOSTNAME`: The hostname of the network service being scanned. Note that
this variable is only available if Tsunami was invoked with a hostname target
(e.g. `hostname.lan`);
- `T_NS_PORT`: The port of the network service being scanned (e.g. `1000`);
- `T_NS_IP`: The IP of the network service being scanned (e.g. `127.0.0.1`);
- `T_CBS_URI`: The callback server URL used to trigger the callback server. This
is the main variable used when using the callback server. It contains the
address and hashed secret (e.g. `http://tsunami-callback.lan/8fe7d878787d65`
where `8fe7d878787d65` is the **hashed** secret);
- `T_CBS_SECRET`: The callback server secret generated for the current workflow
run; note that it is not hashed and is not relevant in most cases (e.g.
`somesecret`);
- `T_CBS_ADDRESS`: Address of the callback server (e.g. `tsunami-callback.lan`);
- `T_CBS_PORT`: Port of the callback server (e.g. `80`);
================================================
FILE: docs/howto/new-detector/templated/glossary-tests-magic-uri.md
================================================
## Magic tests URIs
The following URIs are considered "magic" in tests when using the mock HTTP
server:
- `TSUNAMI_MAGIC_ANY_URI`: Will match any URI; so this answer will match any
request;
- `TSUNAMI_MAGIC_ECHO_SERVER`: Force Tsunami to repeat the request in the
response. This is used internally to detect flaky detectors;
================================================
FILE: docs/howto/orchestration.md
================================================
# Tsunami Scan Orchestration
## Overview
Tsunami follows a hardcoded 2-step process when scanning a publicly
exposed network endpoint:
* **Reconnaissance**: First, Tsunami identifies open ports and
subsequently fingerprints protocols, services and other software running on
the target host via a set of fingerprinting plugins. To not reinvent the
wheel, Tsunami leverages existing tools such as [nmap](https://nmap.org/)
for some of these tasks.
* **Vulnerability verification**: Based on the information gathered in step 1,
Tsunami selects all vulnerability verification plugins matching the
identified services and executes them in order to verify vulnerabilities
without false positives.
## Overall Scanning Workflow
Following diagram shows the overall workflow for a Tsunami scan.

## Reconnaissance
In the reconnaissance step, Tsunami probes the scan target and gathers as much
information about the scan target as possible, including:
* open ports,
* protocols,
* network services & their banners,
* potential software & corresponding version.
Tsunami performs the Reconnaissance step in 2 separate phases.
### Port Scanning Phase
In the port scanning phase, Tsunami performs port sweeping in order to identify
open ports, protocols and network services on the scan target. The output of
Port Scanning is a `PortScanReport` protobuf that contains all the identified
`NetworkService`s from the port scanner.
`PortScanner` is a special type of Tsunami plugins design for Port Scanning
purpose. This allows users to swap the port scanning implementations. To not
reinvent the wheel, users could choose a Tsunami plugin wrapper around existing
tools like [nmap](https://nmap.org/) or
[masscan](https://github.com/robertdavidgraham/masscan). You may find useful
`PortScanner` implementations in
[tsunami-security-scanner-plugins](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google/portscan)
repo.
### Fingerprinting Phase
Usually port scanners only provide very basic service detection capability. When
the scan target hosts complicated network services, like web servers, the
scanner needs to perform further fingerprinting work to learn more about the
exposed network services.
For example, the scan target might choose to serve multiple web applications on
the same TCP port 443 using nginx for reverse proxy, `/blog` for WordPress, and
`/forum` for phpBB, etc. Port scanner will only be able to tell port 443 is
running nginx. A Web Application Fingerprinter with a comprehensive crawler is
required to identify these applications.
`ServiceFingerprinter` is a special type of Tsunami plugin that allows users to
define fingerprinters for a specific network service. By using filtering
annotations, Tsunami will be able to automatically invoke appropriate
`ServiceFingerprinter`s when it identifies matching network services.
Tsunami only performs service fingerprinting for web services,
using the
[`WebServiceFingerprinter`](https://github.com/google/tsunami-security-scanner-plugins/blob/71c57f6bc151a3d97675d74c904a175172c77df4/google/fingerprinters/web/src/main/java/com/google/tsunami/plugins/fingerprinters/web/WebServiceFingerprinter.java)
plugin.
### Reconnaissance Report
At the end of the reconnaissance step, Tsunami compiles both the port scanner
outputs and service fingerprinter outputs into a single `ReconnaissanceReport`
protobuf for Vulnerability Verification.
## Vulnerability Verification
In the Vulnerability Verification step, Tsunami executes the `VulnDetector`
plugins in parallel to verify certain vulnerabilities on the scan target based
on the information gathered in the Reconnaissance step. `VulnDetector`'s
detection logic could either be implemented as plain Java code, or as a separate
binary / script using a different language like python or go. External binaries
and scripts have to be executed as separate processes outside of Tsunami using
Tsunami's command execution util.
### Detector Selection
Usually one `VulnDetector` only verifies one vulnerability and the vulnerability
often only affects one type of network service or software. In order to avoid
doing wasteful work, Tsunami allows plugins to be annotated by some filtering
annotations to limit the scope of the plugin.
Then before the Vulnerability Verification step starts, Tsunami will select
matching `VulnDetector`s to run based on the exposed network services and
running software on the scan target. Non-matching `VulnDetector`s will stay
inactive throughout the entire scan.
================================================
FILE: docs/index.md
================================================
================================================
FILE: full.Dockerfile
================================================
# Core engine
FROM ghcr.io/google/tsunami-scanner-core:latest AS core
# Callback server
FROM ghcr.io/google/tsunami-security-scanner-callback-server:latest AS tcs
# Plugins
FROM ghcr.io/google/tsunami-plugins-google:latest AS plugins-google
FROM ghcr.io/google/tsunami-plugins-templated:latest AS plugins-templated
FROM ghcr.io/google/tsunami-plugins-doyensec:latest AS plugins-doyensec
FROM ghcr.io/google/tsunami-plugins-community:latest AS plugins-community
FROM ghcr.io/google/tsunami-plugins-govtech:latest AS plugins-govtech
FROM ghcr.io/google/tsunami-plugins-facebook:latest AS plugins-facebook
FROM ghcr.io/google/tsunami-plugins-python:latest AS plugins-python
# Release a full version
FROM ubuntu:latest AS release
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates openjdk-21-jre golang python3 python3-venv \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /usr/share/doc && rm -rf /usr/share/man \
&& apt-get clean \
&& mkdir logs/
COPY --from=core /usr/tsunami/ /usr/tsunami/
COPY --from=tcs /usr/tsunami/ /usr/tsunami/
COPY --from=plugins-google /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-templated /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-doyensec /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-community /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-govtech /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-facebook /usr/tsunami/plugins/ /usr/tsunami/plugins/
COPY --from=plugins-python /usr/tsunami/py_plugins/ /usr/tsunami/py_plugins/
# Install the linter
RUN go install github.com/google/tsunami-security-scanner-plugins/templated/utils/linter@latest \
&& ln -s /root/go/bin/linter /usr/bin/tsunami-linter
# Symlink the Python plugins so that they are discoverable by Python.
RUN ln -s /usr/tsunami/py_plugins/ /usr/tsunami/py_server/py_plugins
# Create the __init__.py files to ensure all plugins are discoverable.
RUN find /usr/tsunami/py_plugins/ \
-type d \
! -name '__pycache__' \
-exec touch '{}/__init__.py' \;
# Create wrapper scripts
WORKDIR /usr/tsunami
RUN echo '#!/bin/bash\njava -cp /usr/tsunami/tsunami.jar:/usr/tsunami/plugins/* -Dtsunami.config.location=/usr/tsunami/tsunami.yaml com.google.tsunami.main.cli.TsunamiCli $*\n' > /usr/bin/tsunami \
&& chmod +x /usr/bin/tsunami \
&& echo '#!/bin/bash\njava -cp /usr/tsunami/tsunami-tcs.jar com.google.tsunami.callbackserver.main.TcsMain --custom-config=/usr/tsunami/tcs_config.yaml $*\n' > /usr/bin/tsunami-tcs \
&& chmod +x /usr/bin/tsunami-tcs \
&& echo '#!/bin/bash\n/usr/tsunami/py_venv/bin/python3 /usr/tsunami/py_server/plugin_server.py $*\n' > /usr/bin/tsunami-py-server \
&& chmod +x /usr/bin/tsunami-py-server
================================================
FILE: go.mod
================================================
module github.com/google/tsunami-security-scanner
go 1.22.0
================================================
FILE: main/README.md
================================================
# Tsunami Main
## Overview
This module provides the entry point for starting up Tsunami Security Scanner.
================================================
FILE: main/build.gradle
================================================
plugins {
id 'application'
id 'com.gradleup.shadow' version "8.3.6"
}
description = 'Tsunami: main'
dependencies {
implementation project(':tsunami-common')
implementation project(':tsunami-plugin')
implementation project(':tsunami-proto')
implementation project(':tsunami-workflow')
implementation "com.beust:jcommander:1.48"
implementation "com.doyensec:libajp:1.0.0"
implementation "com.google.cloud:google-cloud-storage:1.103.1"
implementation "com.google.flogger:flogger:0.9"
implementation "com.google.flogger:google-extensions:0.9"
implementation "com.google.guava:guava:33.0.0-jre"
implementation "com.google.inject:guice:6.0.0"
implementation "com.google.protobuf:protobuf-java:3.25.5"
implementation "io.github.classgraph:classgraph:4.8.65"
implementation "io.grpc:grpc-netty:1.60.0"
implementation "javax.inject:javax.inject:1"
implementation "org.jsoup:jsoup:1.9.2"
runtimeOnly "org.glassfish.jaxb:jaxb-runtime:2.3.1"
testImplementation "com.google.truth:truth:1.4.4"
testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4"
testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4"
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-core:5.18.0"
}
application {
mainClassName = 'com.google.tsunami.main.cli.TsunamiCli'
}
shadowJar {
exclude '*.proto'
}
tasks.named("distZip") {
dependsOn(":tsunami-main:shadowJar")
}
tasks.named("distTar") {
dependsOn(":tsunami-main:shadowJar")
}
tasks.named("startScripts") {
dependsOn(":tsunami-main:shadowJar")
}
tasks.named("startShadowScripts") {
dependsOn(":tsunami-main:jar")
}
tasks.named("compileJava") {
dependsOn(":tsunami-plugin:shadowJar")
}
tasks.named('compileJava') {
dependsOn(':tsunami-proto:shadowJar')
dependsOn(':tsunami-workflow:shadowJar')
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/LanguageServerOptions.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.google.tsunami.common.cli.CliOption;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
/** Command line arguments for Tsunami language servers. */
@Parameters(separators = "=")
public final class LanguageServerOptions implements CliOption {
@Parameter(
names = "--plugin-server-paths",
description = "The filename of the language server to run language-specific plugins.")
public List pluginServerFilenames = ImmutableList.of();
@Parameter(
names = "--plugin-server-ports",
description =
"The port of the plugin server to open connection with. If not enough ports were"
+ " specified for the number of language servers specified, an open port will be"
+ " chosen.")
public List pluginServerPorts = ImmutableList.of();
@Parameter(
names = "--plugin-server-rpc-deadline-seconds",
description = "The RPC deadline in seconds for the plugin servers.")
public List pluginServerRpcDeadlineSeconds = ImmutableList.of();
@Parameter(
names = {"--remote-plugin-server-addresses", "--python-plugin-server-address"},
description = "The address for remote language server (e.g. Python).")
public List remotePluginServerAddress = ImmutableList.of();
@Parameter(
names = {"--remote-plugin-server-ports", "--python-plugin-server-port"},
description = "The port of the remote plugin server to open connection with.")
public List remotePluginServerPort = ImmutableList.of();
@Parameter(
names = "--remote-plugin-server-rpc-deadline-seconds",
description = "The RPC deadline in seconds for this plugin server.")
public List remotePluginServerRpcDeadlineSeconds = ImmutableList.of();
@Override
public void validate() {
if (!pluginServerFilenames.isEmpty() || !pluginServerPorts.isEmpty()) {
if (pluginServerFilenames != null && !pluginServerFilenames.isEmpty()) {
for (String pluginServerFilename : pluginServerFilenames) {
if (!Files.exists(Paths.get(pluginServerFilename))) {
throw new ParameterException(
String.format("Language server path %s does not exist", pluginServerFilename));
}
}
}
if (pluginServerPorts != null && !pluginServerPorts.isEmpty()) {
for (String pluginServerPort : pluginServerPorts) {
try {
int port = Integer.parseInt(pluginServerPort);
if (!(port <= NetworkEndpointUtils.MAX_PORT_NUMBER && port > 0)) {
throw new ParameterException(
String.format(
"Port out of range. Expected [0, %s], actual %s.",
NetworkEndpointUtils.MAX_PORT_NUMBER, pluginServerPort));
}
} catch (NumberFormatException e) {
throw new ParameterException(
String.format("Port number must be an integer. Got %s instead.", pluginServerPort),
e);
}
}
}
var pathCounts = pluginServerFilenames == null ? 0 : pluginServerFilenames.size();
var portCounts = pluginServerPorts == null ? 0 : pluginServerPorts.size();
if (pathCounts != portCounts) {
throw new ParameterException(
String.format(
"Number of plugin server paths must be equal to number of plugin server ports."
+ " Paths: %s. Ports: %s.",
pathCounts, portCounts));
}
if (!pluginServerRpcDeadlineSeconds.isEmpty()) {
if (pluginServerRpcDeadlineSeconds.size() != pathCounts) {
throw new ParameterException(
String.format(
"Number of plugin server rpc deadlines must be equal to number of plugin server"
+ " ports. Paths: %s. Ports: %s. Deadlines: %s",
pathCounts, portCounts, pluginServerRpcDeadlineSeconds.size()));
}
}
}
if (!remotePluginServerAddress.isEmpty()) {
var addrCounts = remotePluginServerAddress.size();
var portCounts = remotePluginServerPort.size();
if (addrCounts != portCounts) {
throw new ParameterException(
String.format(
"Number of remote plugin server paths must be equal to number of plugin server "
+ "ports. Addresses: %s. Ports: %s.",
addrCounts, portCounts));
}
if (!remotePluginServerRpcDeadlineSeconds.isEmpty()) {
if (remotePluginServerRpcDeadlineSeconds.size() != addrCounts) {
throw new ParameterException(
String.format(
"Number of plugin server rpc deadlines must be equal to number of plugin server"
+ " ports. Paths: %s. Ports: %s. Deadlines: %s",
addrCounts, portCounts, pluginServerRpcDeadlineSeconds.size()));
}
}
for (int port : remotePluginServerPort) {
if (!(port <= NetworkEndpointUtils.MAX_PORT_NUMBER && port > 0)) {
throw new ParameterException(
String.format(
"Remote plugin server port out of range. Expected [0, %s], actual %s.",
NetworkEndpointUtils.MAX_PORT_NUMBER, port));
}
}
}
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/ScanResultsArchiver.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver.GS_URL_PATTERN;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import com.google.common.base.Strings;
import com.google.common.flogger.GoogleLogger;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.google.tsunami.common.cli.CliOption;
import com.google.tsunami.common.io.archiving.Archiver;
import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiver;
import com.google.tsunami.common.io.archiving.RawFileArchiver;
import com.google.tsunami.main.cli.option.OutputDataFormat;
import com.google.tsunami.proto.ScanResults;
import javax.inject.Inject;
class ScanResultsArchiver {
@Parameters(separators = "=")
static final class Options implements CliOption {
@Parameter(
names = "--scan-results-local-output-filename",
description = "The local output filename of the scanning results.")
public String localOutputFilename;
@Parameter(
names = "--scan-results-local-output-format",
description = "The format of the scanning results saved as local file.")
public OutputDataFormat localOutputFormat;
@Parameter(
names = "--scan-results-gcs-output-file-url",
description = "The GCS file url for the uploaded scanning results.")
public String gcsOutputFileUrl;
@Parameter(
names = "--scan-results-gcs-output-format",
description = "The format of the scanning results uploaded to GCS bucket.")
public OutputDataFormat gcsOutputFormat;
@Parameter(
names = "--scan-results-logging-enabled",
description = "Enable logging of the scan results.",
arity = 1)
public Boolean loggingEnabled = false;
@Override
public void validate() {
if (!Strings.isNullOrEmpty(gcsOutputFileUrl)
&& !GS_URL_PATTERN.matcher(gcsOutputFileUrl).matches()) {
throw new ParameterException(String.format("Malformed GCS URL: '%s'", gcsOutputFileUrl));
}
}
}
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final Options options;
private final RawFileArchiver rawFileArchiver;
private final GoogleCloudStorageArchiver.Factory googleCloudStorageArchiverFactory;
@Inject
// TODO(b/145315535): inject archivers using multibinder instead.
ScanResultsArchiver(
Options options,
RawFileArchiver rawFileArchiver,
GoogleCloudStorageArchiver.Factory googleCloudStorageArchiverFactory) {
this.options = checkNotNull(options);
this.rawFileArchiver = checkNotNull(rawFileArchiver);
this.googleCloudStorageArchiverFactory = checkNotNull(googleCloudStorageArchiverFactory);
}
Storage getGcsStorage() {
return StorageOptions.getDefaultInstance().getService();
}
void archive(ScanResults scanResults) throws InvalidProtocolBufferException {
if (!Strings.isNullOrEmpty(options.localOutputFilename)) {
archive(rawFileArchiver, options.localOutputFilename, options.localOutputFormat, scanResults);
}
if (!Strings.isNullOrEmpty(options.gcsOutputFileUrl)) {
GoogleCloudStorageArchiver archiver =
googleCloudStorageArchiverFactory.create(getGcsStorage());
archive(archiver, options.gcsOutputFileUrl, options.gcsOutputFormat, scanResults);
}
if (options.loggingEnabled) {
logger.atInfo().log("Scan results for RVD efficacy: %s", scanResults);
}
}
private static void archive(
Archiver archiver, String location, OutputDataFormat outputFormat, ScanResults scanResults)
throws InvalidProtocolBufferException {
switch (outputFormat) {
case BIN_PROTO:
archiver.archive(location, scanResults.toByteArray());
break;
case JSON:
archiver.archive(location, JsonFormat.printer().print(scanResults));
break;
}
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/ScanResultsArchiverModule.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import com.google.inject.AbstractModule;
/** Installs {@link ScanResultsArchiver}. */
final class ScanResultsArchiverModule extends AbstractModule {
@Override
protected void configure() {
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/TsunamiCli.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname;
import static com.google.tsunami.common.data.NetworkEndpointUtils.forIp;
import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndHostname;
import static com.google.tsunami.common.data.NetworkServiceUtils.buildUriNetworkService;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.flogger.GoogleLogger;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.tsunami.common.cli.CliOptionsModule;
import com.google.tsunami.common.command.CommandExecutorModule;
import com.google.tsunami.common.config.ConfigLoader;
import com.google.tsunami.common.config.ConfigModule;
import com.google.tsunami.common.config.TsunamiConfig;
import com.google.tsunami.common.config.YamlConfigLoader;
import com.google.tsunami.common.io.archiving.GoogleCloudStorageArchiverModule;
import com.google.tsunami.common.net.http.HttpClientCliOptions;
import com.google.tsunami.common.net.http.HttpClientModule;
import com.google.tsunami.common.net.socket.TsunamiSocketFactoryModule;
import com.google.tsunami.common.reflection.ClassGraphModule;
import com.google.tsunami.common.server.LanguageServerCommand;
import com.google.tsunami.common.time.SystemUtcClockModule;
import com.google.tsunami.main.cli.option.MainCliOptions;
import com.google.tsunami.main.cli.server.RemoteServerLoader;
import com.google.tsunami.main.cli.server.RemoteServerLoaderModule;
import com.google.tsunami.plugin.PluginExecutionModule;
import com.google.tsunami.plugin.PluginLoadingModule;
import com.google.tsunami.plugin.RemoteVulnDetectorLoadingModule;
import com.google.tsunami.plugin.payload.PayloadGeneratorModule;
import com.google.tsunami.proto.ScanResults;
import com.google.tsunami.proto.ScanStatus;
import com.google.tsunami.proto.ScanTarget;
import com.google.tsunami.workflow.AdvisoriesWorkflow;
import com.google.tsunami.workflow.DefaultScanningWorkflow;
import com.google.tsunami.workflow.ScanningWorkflowException;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import java.io.IOException;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
/** Command line interface for the Tsunami Security Scanner. */
public final class TsunamiCli {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final DefaultScanningWorkflow scanningWorkflow;
private final AdvisoriesWorkflow advisoriesWorkflow;
private final ScanResultsArchiver scanResultsArchiver;
private final MainCliOptions mainCliOptions;
private final RemoteServerLoader remoteServerLoader;
@Inject
TsunamiCli(
DefaultScanningWorkflow scanningWorkflow,
AdvisoriesWorkflow advisoriesWorkflow,
ScanResultsArchiver scanResultsArchiver,
MainCliOptions mainCliOptions,
RemoteServerLoader remoteServerLoader) {
this.scanningWorkflow = checkNotNull(scanningWorkflow);
this.advisoriesWorkflow = checkNotNull(advisoriesWorkflow);
this.scanResultsArchiver = checkNotNull(scanResultsArchiver);
this.mainCliOptions = checkNotNull(mainCliOptions);
this.remoteServerLoader = checkNotNull(remoteServerLoader);
}
public boolean run()
throws ExecutionException, InterruptedException, ScanningWorkflowException, IOException {
String logId = mainCliOptions.getLogId();
// TODO(b/171405612): Find a way to print the log ID at every log line.
logger.atInfo().log("%sTsunamiCli starting...", logId);
ImmutableList languageServerProcesses = remoteServerLoader.runServerProcesses();
if (mainCliOptions.dumpAdvisoriesPath != null && !mainCliOptions.dumpAdvisoriesPath.isEmpty()) {
logger.atInfo().log("No scan will be performed. Dumping advisories.");
advisoriesWorkflow.run(mainCliOptions.dumpAdvisoriesPath);
return true;
}
ScanResults scanResults = scanningWorkflow.run(buildScanTarget());
languageServerProcesses.forEach(Process::destroy);
logger.atInfo().log("Tsunami scan finished, saving results.");
saveResults(scanResults);
if (hasSuccessfulResults(scanResults)) {
logger.atInfo().log("TsunamiCli finished...");
return true;
} else {
logger.atInfo().log(
"Tsunami scan has failed status, message = %s.", scanResults.getStatusMessage());
return false;
}
}
private static boolean hasSuccessfulResults(ScanResults scanResults) {
return scanResults.getScanStatus().equals(ScanStatus.SUCCEEDED)
|| scanResults.getScanStatus().equals(ScanStatus.PARTIALLY_SUCCEEDED);
}
private ScanTarget buildScanTarget() {
ScanTarget.Builder scanTargetBuilder = ScanTarget.newBuilder();
String ip = null;
if (mainCliOptions.ipV4Target != null) {
ip = mainCliOptions.ipV4Target;
} else if (mainCliOptions.ipV6Target != null) {
ip = mainCliOptions.ipV6Target;
}
if (ip != null && mainCliOptions.hostnameTarget != null) {
scanTargetBuilder.setNetworkEndpoint(forIpAndHostname(ip, mainCliOptions.hostnameTarget));
} else if (ip != null) {
scanTargetBuilder.setNetworkEndpoint(forIp(ip));
} else if (mainCliOptions.uriTarget != null) {
scanTargetBuilder.setNetworkService(buildUriNetworkService(mainCliOptions.uriTarget));
} else {
scanTargetBuilder.setNetworkEndpoint(forHostname(mainCliOptions.hostnameTarget));
}
return scanTargetBuilder.build();
}
private void saveResults(ScanResults scanResults) throws IOException {
scanResultsArchiver.archive(scanResults);
}
private static final class TsunamiCliFirstStageModule extends AbstractModule {
private final ScanResult classScanResult;
private final String[] args;
private final TsunamiConfig tsunamiConfig;
TsunamiCliFirstStageModule(
ScanResult classScanResult, String[] args, TsunamiConfig tsunamiConfig) {
this.classScanResult = checkNotNull(classScanResult);
this.args = checkNotNull(args);
this.tsunamiConfig = checkNotNull(tsunamiConfig);
}
@Override
protected void configure() {
install(new ClassGraphModule(classScanResult));
install(new ConfigModule(classScanResult, tsunamiConfig));
install(new CliOptionsModule(classScanResult, "TsunamiCli", args));
}
}
private static final class TsunamiCliModule extends AbstractModule {
private final ScanResult classScanResult;
private final Injector parentInjector;
private final TsunamiConfig tsunamiConfig;
TsunamiCliModule(
Injector parentInjector, ScanResult classScanResult, TsunamiConfig tsunamiConfig) {
this.classScanResult = checkNotNull(classScanResult);
this.parentInjector = checkNotNull(parentInjector);
this.tsunamiConfig = checkNotNull(tsunamiConfig);
}
@Override
protected void configure() {
MainCliOptions mco = parentInjector.getInstance(MainCliOptions.class);
LanguageServerOptions lso = parentInjector.getInstance(LanguageServerOptions.class);
HttpClientCliOptions hcco = parentInjector.getInstance(HttpClientCliOptions.class);
ScanResultsArchiver.Options srao =
parentInjector.getInstance(ScanResultsArchiver.Options.class);
ImmutableList commands = extractPluginServerArgs(mco, lso, hcco, srao);
install(new SystemUtcClockModule());
install(new CommandExecutorModule());
install(new HttpClientModule.Builder().setLogId(mco.getLogId()).build());
install(new TsunamiSocketFactoryModule());
install(new GoogleCloudStorageArchiverModule());
install(new ScanResultsArchiverModule());
install(new PluginExecutionModule());
install(new PluginLoadingModule(classScanResult));
install(new PayloadGeneratorModule(new SecureRandom()));
install(new RemoteServerLoaderModule(commands));
install(new RemoteVulnDetectorLoadingModule(commands));
}
private ImmutableList extractPluginServerArgs(
MainCliOptions mco,
LanguageServerOptions lso,
HttpClientCliOptions hcco,
ScanResultsArchiver.Options srao) {
List commands = Lists.newArrayList();
Boolean trustAllSslCertCli = hcco.trustAllCertificates;
var logId = mco.getLogId();
var paths = lso.pluginServerFilenames;
var ports = lso.pluginServerPorts;
var rpcDeadline = lso.pluginServerRpcDeadlineSeconds;
var remoteServerAddresses = lso.remotePluginServerAddress;
var remoteServerPorts = lso.remotePluginServerPort;
var remoteRpcDeadlines = lso.remotePluginServerRpcDeadlineSeconds;
if (paths.isEmpty() && remoteServerAddresses.isEmpty()) {
return ImmutableList.of();
}
Map callbackConfig = tsunamiConfig.readConfigValue("plugin.callbackserver");
Map httpClientConfig = tsunamiConfig.readConfigValue("common.net.http");
boolean trustAllSslCertConfig =
(boolean) httpClientConfig.getOrDefault("trust_all_certificates", false);
String lngOutputDir = extractOutputDir(srao);
boolean lngTrustAllSslCertCli =
trustAllSslCertCli != null ? trustAllSslCertCli.booleanValue() : trustAllSslCertConfig;
Duration lngConnectDuration =
Duration.ofSeconds((int) httpClientConfig.getOrDefault("connect_timeout_seconds", 0));
String lngCallbackAddress = (String) callbackConfig.getOrDefault("callback_address", "");
Integer lngCallbackPort = (Integer) callbackConfig.getOrDefault("callback_port", 0);
String lngPollingUri = (String) callbackConfig.getOrDefault("polling_uri", "");
for (int i = 0; i < paths.size(); ++i) {
commands.add(
LanguageServerCommand.create(
paths.get(i),
"",
ports.get(i),
logId,
lngOutputDir,
lngTrustAllSslCertCli,
lngConnectDuration,
lngCallbackAddress,
lngCallbackPort,
lngPollingUri,
rpcDeadline.isEmpty() ? 0 : rpcDeadline.get(i)));
}
for (int i = 0; i < remoteServerAddresses.size(); ++i) {
commands.add(
LanguageServerCommand.create(
"",
remoteServerAddresses.get(i),
remoteServerPorts.get(i).toString(),
logId,
lngOutputDir,
lngTrustAllSslCertCli,
lngConnectDuration,
lngCallbackAddress,
lngCallbackPort,
lngPollingUri,
remoteRpcDeadlines.isEmpty() ? 0 : remoteRpcDeadlines.get(i)));
}
return ImmutableList.copyOf(commands);
}
private String extractOutputDir(ScanResultsArchiver.Options sra) {
if (!Strings.isNullOrEmpty(sra.localOutputFilename)) {
return Path.of(sra.localOutputFilename).getParent().toString();
}
return "";
}
}
public static int doMain(String[] args) {
Stopwatch stopwatch = Stopwatch.createStarted();
TsunamiConfig tsunamiConfig = loadConfig();
try (ScanResult scanResult =
new ClassGraph()
.enableAllInfo()
.blacklistPackages("com.google.tsunami.plugin.testing")
.scan()) {
logger.atInfo().log("Full classpath scan took %s", stopwatch);
Injector firstStageInjector =
Guice.createInjector(new TsunamiCliFirstStageModule(scanResult, args, tsunamiConfig));
Injector injector =
firstStageInjector.createChildInjector(
new TsunamiCliModule(firstStageInjector, scanResult, tsunamiConfig));
// Exit with non-zero code if scan failed.
if (!injector.getInstance(TsunamiCli.class).run()) {
return 1;
}
logger.atInfo().log("Full Tsunami scan took %s.", stopwatch.stop());
return 0;
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Exiting due to workflow execution exceptions.");
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return 1;
}
}
public static void main(String[] args) {
System.exit(doMain(args));
}
private static TsunamiConfig loadConfig() {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
ConfigLoader configLoader;
Optional loaderClass = TsunamiConfig.getSystemProperty("tsunami.config.loader");
if (loaderClass.isPresent()
&& scanResult.getAllClassesAsMap().containsKey(loaderClass.get())) {
configLoader =
scanResult
.getClassInfo(loaderClass.get())
.loadClass(ConfigLoader.class)
.getConstructor()
.newInstance();
} else {
configLoader = new YamlConfigLoader();
}
return configLoader.loadConfig();
} catch (ReflectiveOperationException e) {
throw new LinkageError("Error loading config.", e);
}
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/option/MainCliOptions.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.tsunami.common.cli.CliOption;
import com.google.tsunami.main.cli.option.validator.IpV4Validator;
import com.google.tsunami.main.cli.option.validator.IpV6Validator;
import java.util.ArrayList;
import java.util.List;
/** Command line arguments for Tsunami. */
@Parameters(separators = "=")
public final class MainCliOptions implements CliOption {
@Parameter(
names = "--ip-v4-target",
description = "The IP v4 address of the scanning target.",
validateWith = IpV4Validator.class)
public String ipV4Target;
@Parameter(
names = "--ip-v6-target",
description = "The IP v6 address of the scanning target.",
validateWith = IpV6Validator.class)
public String ipV6Target;
@Parameter(names = "--hostname-target", description = "The hostname of the scanning target.")
public String hostnameTarget;
@Parameter(names = "--log-id", description = "A log ID to print in front of the logs.")
public String logId;
@Parameter(
names = "--uri-target",
description =
"The URI of the scanning target that supports both http & https schemes. When this"
+ " parameter is set, port scan is automatically skipped.")
public String uriTarget;
@Parameter(
names = "--dump-advisories",
description =
"Disable scanning. Reports the list of currently enabled advisories to the specified"
+ " file, in textproto format.")
public String dumpAdvisoriesPath;
@Override
public void validate() {
if (dumpAdvisoriesPath != null && !dumpAdvisoriesPath.isEmpty()) {
return;
}
List portScanEnabledTargets = new ArrayList<>();
List portScanDisabledTargets = new ArrayList<>();
if (ipV4Target != null) {
portScanEnabledTargets.add("--ip-v4-target");
}
if (ipV6Target != null) {
portScanEnabledTargets.add("--ip-v6-target");
}
if (hostnameTarget != null) {
portScanEnabledTargets.add("--hostname-target");
}
if (uriTarget != null) {
portScanDisabledTargets.add("--uri-target");
}
if (portScanEnabledTargets.isEmpty() && portScanDisabledTargets.isEmpty()) {
throw new ParameterException(
"One of the following parameters is expected: --ip-v4-target, --ip-v6-target,"
+ " --hostname-target, --uri-target");
}
if (!portScanEnabledTargets.isEmpty() && !portScanDisabledTargets.isEmpty()) {
throw new ParameterException(
"Parameters that require port scan (--ip-v4-target, --ip-v6-target, --hostname-target)"
+ " should not be passed along with parameters that skip port scan (--uri-target)");
}
}
/** Returns the log ID to print in front of the logs. */
public String getLogId() {
return (logId == null) ? "" : (logId + ": ");
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/option/OutputDataFormat.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option;
import com.google.common.base.Ascii;
import java.util.Optional;
/** Output format of Tsunami's scanning results. */
public enum OutputDataFormat {
BIN_PROTO,
JSON;
/**
* Parses the given {@code value} into {@link OutputDataFormat} enum.
*
* @param value the string representation of the {@link OutputDataFormat} enum.
* @return the parsed {@link OutputDataFormat} enum.
*/
public static Optional parse(String value) {
for (OutputDataFormat outputDataFormat : OutputDataFormat.values()) {
if (Ascii.equalsIgnoreCase(outputDataFormat.name(), value)) {
return Optional.of(outputDataFormat);
}
}
return Optional.empty();
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpV4Validator.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import java.net.Inet4Address;
import java.net.InetAddress;
/** Command line flag validator for an IP v4 address. */
public class IpV4Validator extends IpValidator {
@Override
protected int ipVersion() {
return 4;
}
@Override
protected boolean shouldAccept(InetAddress inetAddress) {
return inetAddress instanceof Inet4Address;
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpV6Validator.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import java.net.Inet6Address;
import java.net.InetAddress;
/** Command line flag validator for an IP v6 address. */
public class IpV6Validator extends IpValidator {
@Override
protected int ipVersion() {
return 6;
}
@Override
protected boolean shouldAccept(InetAddress inetAddress) {
return inetAddress instanceof Inet6Address;
}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/option/validator/IpValidator.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import com.beust.jcommander.IParameterValidator;
import com.beust.jcommander.ParameterException;
import com.google.common.base.Strings;
import com.google.common.net.InetAddresses;
import java.net.InetAddress;
/** Base command line flag validator for an IP address. */
public abstract class IpValidator implements IParameterValidator {
@Override
public void validate(String name, String value) {
if (Strings.isNullOrEmpty(value)
|| !InetAddresses.isInetAddress(value)
|| !shouldAccept(InetAddresses.forString(value))) {
throw new ParameterException(
String.format(
"Parameter %s should point to a valid IP v%d address, got '%s'",
name, ipVersion(), value));
}
}
protected abstract int ipVersion();
protected abstract boolean shouldAccept(InetAddress inetAddress);
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/server/RemoteServerLoader.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.server;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.tsunami.common.command.CommandExecutor;
import com.google.tsunami.common.command.CommandExecutorFactory;
import com.google.tsunami.common.server.LanguageServerCommand;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import javax.inject.Qualifier;
/** Loader to run language servers. */
public class RemoteServerLoader {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final List commands;
@Inject
RemoteServerLoader(@LanguageServerCommands List commands) {
this.commands = checkNotNull(commands);
}
public ImmutableList runServerProcesses() {
logger.atInfo().log("Starting language server processes (if any)...");
return commands.stream()
// Filter out commands that don't need server start up
.filter(command -> !Strings.isNullOrEmpty(command.serverCommand()))
.map(
command ->
runProcess(
CommandExecutorFactory.create(
command.serverCommand(),
getCommand("--port=", command.port()),
getCommand("--log_id=", command.logId()),
getCommand("--log_output=", command.outputDir()),
"--trust_all_ssl_cert=" + command.trustAllSslCert(),
getCommand("--timeout_seconds=", command.timeoutSeconds().getSeconds()),
getCommand("--callback_address=", command.callbackAddress()),
getCommand("--callback_port=", command.callbackPort()),
getCommand("--polling_uri=", command.pollingUri()))))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList());
}
private String getCommand(String flag, Object command) {
return command.toString().isEmpty() || command.toString().equals("0") ? "" : flag + command;
}
private Optional runProcess(CommandExecutor executor) {
try {
return Optional.of(executor.executeAsync());
} catch (IOException | InterruptedException | ExecutionException e) {
logger.atWarning().withCause(e).log("Could not execute language server binary.");
}
return Optional.empty();
}
/** Guice interface for injecting {@link LanguageServerCommand} object lists. */
@Qualifier
@Retention(RUNTIME)
public @interface LanguageServerCommands {}
}
================================================
FILE: main/src/main/java/com/google/tsunami/main/cli/server/RemoteServerLoaderModule.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.server;
import com.google.common.collect.ImmutableList;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.tsunami.common.server.LanguageServerCommand;
import java.util.List;
/** Installs {@link RemoteServerLoaderModule}. */
public final class RemoteServerLoaderModule extends AbstractModule {
private final ImmutableList commands;
public RemoteServerLoaderModule(ImmutableList commands) {
this.commands = commands;
}
@Provides
@RemoteServerLoader.LanguageServerCommands
List provideLanguageServerCommands() {
return commands;
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/LanguageServerOptionsTest.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class LanguageServerOptionsTest {
@Test
public void validate_whenPluginServerFilenameDoesNotExist_throwsParameterException() {
LanguageServerOptions options = new LanguageServerOptions();
options.pluginServerFilenames = ImmutableList.of("nonexistingfile");
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_whenPortNumberNotInteger_throwsParameterException() {
LanguageServerOptions options = new LanguageServerOptions();
options.pluginServerPorts = ImmutableList.of("test");
assertThrows(ParameterException.class, options::validate);
}
@Test
public void validate_whenPortNumberOutOfRange_throwsParameterException() {
LanguageServerOptions options = new LanguageServerOptions();
options.pluginServerPorts = ImmutableList.of("34567", "-1");
assertThrows(
"Port out of range. Expected [0, "
+ NetworkEndpointUtils.MAX_PORT_NUMBER
+ "]"
+ ", actual -1",
ParameterException.class,
options::validate);
}
@Test
public void validate_whenPythonPluginServerPortNumberOutOfRange_throwsParameterException() {
LanguageServerOptions options = new LanguageServerOptions();
options.remotePluginServerAddress = ImmutableList.of("127.0.0.1");
options.remotePluginServerPort = ImmutableList.of(-1);
assertThrows(
"Remote plugin server port out of range. Expected [0, "
+ NetworkEndpointUtils.MAX_PORT_NUMBER
+ "]"
+ ", actual -1",
ParameterException.class,
options::validate);
}
@Test
public void validate_whenPythonPluginServerInvalidNumberOfDeadlines_throwsParameterException() {
LanguageServerOptions options = new LanguageServerOptions();
options.remotePluginServerAddress = ImmutableList.of("127.0.0.1");
options.remotePluginServerPort = ImmutableList.of(10000);
options.remotePluginServerRpcDeadlineSeconds = ImmutableList.of(100, 200);
assertThrows(
"Number of plugin server rpc deadlines must be equal to number of plugin server. ports."
+ " Paths: 1. Ports: 1. Deadlines: 2",
ParameterException.class,
options::validate);
}
@Test
public void validate_whenPythonPluginServerValidNumberOfDeadlines_succeeds() {
LanguageServerOptions options = new LanguageServerOptions();
options.remotePluginServerAddress = ImmutableList.of("127.0.0.1");
options.remotePluginServerPort = ImmutableList.of(10000);
options.remotePluginServerRpcDeadlineSeconds = ImmutableList.of(150);
options.validate();
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/ScanResultsArchiverTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import com.beust.jcommander.ParameterException;
import com.google.cloud.storage.Storage;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Provides;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.google.tsunami.common.io.archiving.testing.FakeGoogleCloudStorageArchivers;
import com.google.tsunami.common.io.archiving.testing.FakeGoogleCloudStorageArchiversModule;
import com.google.tsunami.common.io.archiving.testing.FakeRawFileArchiver;
import com.google.tsunami.common.io.archiving.testing.FakeRawFileArchiverModule;
import com.google.tsunami.main.cli.option.OutputDataFormat;
import com.google.tsunami.proto.ScanResults;
import com.google.tsunami.proto.ScanStatus;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Inject;
import javax.inject.Qualifier;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link ScanResultsArchiver}. */
@RunWith(JUnit4.class)
public final class ScanResultsArchiverTest {
private static final ScanResults SCAN_RESULTS =
ScanResults.newBuilder().setScanStatus(ScanStatus.SUCCEEDED).build();
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock Storage mockStorage;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
private @interface SpyArchiver {}
@Inject private FakeRawFileArchiver fakeRawFileArchiver;
@Inject private FakeGoogleCloudStorageArchivers fakeGoogleCloudStorageArchivers;
@Inject private ScanResultsArchiver.Options options;
@Inject @SpyArchiver private ScanResultsArchiver scanResultsArchiver;
@Before
public void setUp() {
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
bind(ScanResultsArchiver.Options.class)
.toInstance(new ScanResultsArchiver.Options());
install(new ScanResultsArchiverModule());
install(new FakeRawFileArchiverModule());
install(new FakeGoogleCloudStorageArchiversModule());
}
// TODO(b/145315535): wrap GCS API into a client library to get rid of this spy.
@Provides
@SpyArchiver
ScanResultsArchiver getScanResultsArchiverSpy(ScanResultsArchiver delegate) {
return spy(delegate);
}
})
.injectMembers(this);
}
@Test
public void optionsValidate_whenInvalidGcsUrl_throwsParameterException() {
options.gcsOutputFileUrl = "invalid_url";
assertThrows(ParameterException.class, options::validate);
}
@Test
public void optionsValidate_defaultLoggingEnabled_isFalse() {
assertThat(options.loggingEnabled).isFalse();
}
@Test
public void archive_withNoStorageEnabled_storesNothing() throws InvalidProtocolBufferException {
options.localOutputFilename = "";
options.gcsOutputFileUrl = "";
scanResultsArchiver.archive(SCAN_RESULTS);
fakeRawFileArchiver.assertNoDataStored();
fakeGoogleCloudStorageArchivers.assertNoDataStored();
}
@Test
public void archive_withLocalFileEnabledForJsonOutput_storesStringDataLocally()
throws InvalidProtocolBufferException {
options.localOutputFilename = "/tmp/result.json";
options.localOutputFormat = OutputDataFormat.JSON;
options.gcsOutputFileUrl = "";
scanResultsArchiver.archive(SCAN_RESULTS);
assertThat(
parseJsonScanResults(
fakeRawFileArchiver.getStoredCharSequence("/tmp/result.json").toString()))
.isEqualTo(SCAN_RESULTS);
fakeGoogleCloudStorageArchivers.assertNoDataStored();
}
@Test
public void archive_withLocalFileEnabledForBinProtoOutput_storesBytesDataLocally()
throws InvalidProtocolBufferException {
options.localOutputFilename = "/tmp/test.binproto";
options.localOutputFormat = OutputDataFormat.BIN_PROTO;
options.gcsOutputFileUrl = "";
scanResultsArchiver.archive(SCAN_RESULTS);
assertThat(
ScanResults.parseFrom(
fakeRawFileArchiver.getStoredByteArrays("/tmp/test.binproto")))
.isEqualTo(SCAN_RESULTS);
fakeGoogleCloudStorageArchivers.assertNoDataStored();
}
@Test
public void archive_withGcsEnabledForJsonOutput_uploadsStringDataToGcs()
throws InvalidProtocolBufferException {
options.localOutputFilename = "";
options.gcsOutputFileUrl = "gs://test/object/result.json";
options.gcsOutputFormat = OutputDataFormat.JSON;
doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage();
scanResultsArchiver.archive(SCAN_RESULTS);
assertThat(
parseJsonScanResults(
fakeGoogleCloudStorageArchivers
.getStoredCharSequence(mockStorage, "gs://test/object/result.json")
.toString()))
.isEqualTo(SCAN_RESULTS);
fakeRawFileArchiver.assertNoDataStored();
}
@Test
public void archive_withGcsEnabledForBinProtoOutput_uploadsBytesDataToGcs()
throws InvalidProtocolBufferException {
options.localOutputFilename = "";
options.gcsOutputFileUrl = "gs://test/object/result.binproto";
options.gcsOutputFormat = OutputDataFormat.BIN_PROTO;
doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage();
scanResultsArchiver.archive(SCAN_RESULTS);
assertThat(
ScanResults.parseFrom(
fakeGoogleCloudStorageArchivers.getStoredByteArrays(
mockStorage, "gs://test/object/result.binproto")))
.isEqualTo(SCAN_RESULTS);
fakeRawFileArchiver.assertNoDataStored();
}
@Test
public void archive_withLocalAndGcsOptionEnabled_archivesToBothLocation()
throws InvalidProtocolBufferException {
options.localOutputFilename = "/tmp/result.json";
options.localOutputFormat = OutputDataFormat.JSON;
options.gcsOutputFileUrl = "gs://test/object/result.binproto";
options.gcsOutputFormat = OutputDataFormat.BIN_PROTO;
doReturn(mockStorage).when(scanResultsArchiver).getGcsStorage();
scanResultsArchiver.archive(SCAN_RESULTS);
assertThat(
parseJsonScanResults(
fakeRawFileArchiver.getStoredCharSequence("/tmp/result.json").toString()))
.isEqualTo(SCAN_RESULTS);
assertThat(
ScanResults.parseFrom(
fakeGoogleCloudStorageArchivers.getStoredByteArrays(
mockStorage, "gs://test/object/result.binproto")))
.isEqualTo(SCAN_RESULTS);
}
private static ScanResults parseJsonScanResults(String jsonScanResults)
throws InvalidProtocolBufferException {
ScanResults.Builder scanResultsBuilder = ScanResults.newBuilder();
JsonFormat.parser().merge(jsonScanResults, scanResultsBuilder);
return scanResultsBuilder.build();
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/TsunamiCliTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.tsunami.common.cli.CliOptionsModule;
import com.google.tsunami.common.config.ConfigModule;
import com.google.tsunami.common.config.TsunamiConfig;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import com.google.tsunami.common.net.http.HttpClientModule;
import com.google.tsunami.common.time.testing.FakeUtcClockModule;
import com.google.tsunami.main.cli.server.RemoteServerLoaderModule;
import com.google.tsunami.plugin.payload.PayloadGeneratorModule;
import com.google.tsunami.plugin.testing.FailedVulnDetectorBootstrapModule;
import com.google.tsunami.plugin.testing.FakePluginExecutionModule;
import com.google.tsunami.plugin.testing.FakePortScanner;
import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule;
import com.google.tsunami.plugin.testing.FakePortScannerBootstrapModule2;
import com.google.tsunami.plugin.testing.FakeServiceFingerprinter;
import com.google.tsunami.plugin.testing.FakeServiceFingerprinterBootstrapModule;
import com.google.tsunami.plugin.testing.FakeVulnDetector;
import com.google.tsunami.plugin.testing.FakeVulnDetector2;
import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule;
import com.google.tsunami.plugin.testing.FakeVulnDetectorBootstrapModule2;
import com.google.tsunami.proto.AddressFamily;
import com.google.tsunami.proto.DetectionReport;
import com.google.tsunami.proto.Hostname;
import com.google.tsunami.proto.IpAddress;
import com.google.tsunami.proto.NetworkEndpoint;
import com.google.tsunami.proto.NetworkService;
import com.google.tsunami.proto.Port;
import com.google.tsunami.proto.ReconnaissanceReport;
import com.google.tsunami.proto.ScanFinding;
import com.google.tsunami.proto.ScanResults;
import com.google.tsunami.proto.ScanStatus;
import com.google.tsunami.proto.ServiceContext;
import com.google.tsunami.proto.TargetInfo;
import com.google.tsunami.proto.TransportProtocol;
import com.google.tsunami.proto.WebServiceContext;
import com.google.tsunami.workflow.ScanningWorkflowException;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link TsunamiCli}. */
@RunWith(JUnit4.class)
public final class TsunamiCliTest {
private static final String IP_TARGET = "127.0.0.1";
private static final String HOSTNAME_TARGET = "localhost";
private static final String URI_TARGET = "https://localhost/function1";
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
@Mock ScanResultsArchiver scanResultsArchiver;
@Captor ArgumentCaptor scanResultsCaptor;
@Inject private TsunamiCli tsunamiCli;
private boolean runCli(ImmutableMap rawConfigData, String... args)
throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
bind(ScanResultsArchiver.class).toInstance(scanResultsArchiver);
install(new HttpClientModule.Builder().build());
install(new PayloadGeneratorModule(new SecureRandom()));
install(new ConfigModule(scanResult, TsunamiConfig.fromYamlData(rawConfigData)));
install(new CliOptionsModule(scanResult, "TsunamiCliTest", args));
install(new FakeUtcClockModule());
install(new FakePluginExecutionModule());
install(new FakePortScannerBootstrapModule());
install(new FakePortScannerBootstrapModule2());
install(new FakeServiceFingerprinterBootstrapModule());
install(new FakeVulnDetectorBootstrapModule());
install(new FakeVulnDetectorBootstrapModule2());
install(new RemoteServerLoaderModule(ImmutableList.of()));
}
})
.injectMembers(this);
return tsunamiCli.run();
}
}
@Test
public void run_whenIpTarget_generatesAndArchivesCorrectResult()
throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException {
NetworkService expectedNetworkService =
FakeServiceFingerprinter.addWebServiceContext(
FakePortScanner.getFakeNetworkService(NetworkEndpointUtils.forIp(IP_TARGET)));
boolean scanSucceeded = runCli(ImmutableMap.of(), "--ip-v4-target=" + IP_TARGET);
assertThat(scanSucceeded).isTrue();
TargetInfo targetInfo =
TargetInfo.newBuilder().addNetworkEndpoints(NetworkEndpointUtils.forIp(IP_TARGET)).build();
verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture());
ScanResults storedScanResult = scanResultsCaptor.getValue();
assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED);
assertThat(storedScanResult.getScanFindingsList())
.containsExactlyElementsIn(
Stream.of(
FakeVulnDetector.getFakeDetectionReport(targetInfo, expectedNetworkService),
FakeVulnDetector2.getFakeDetectionReport(targetInfo, expectedNetworkService))
.map(TsunamiCliTest::buildScanFindingFromDetectionReport)
.toArray());
assertThat(storedScanResult.getReconnaissanceReport())
.isEqualTo(
ReconnaissanceReport.newBuilder()
.setTargetInfo(
TargetInfo.newBuilder()
.addNetworkEndpoints(NetworkEndpointUtils.forIp(IP_TARGET)))
.addNetworkServices(
FakeServiceFingerprinter.addWebServiceContext(
FakePortScanner.getFakeNetworkService(
NetworkEndpointUtils.forIp(IP_TARGET))))
.build());
}
@Test
public void run_whenHostnameTarget_generatesAndArchivesCorrectResult()
throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException {
NetworkService expectedNetworkService =
FakeServiceFingerprinter.addWebServiceContext(
FakePortScanner.getFakeNetworkService(
NetworkEndpointUtils.forHostname(HOSTNAME_TARGET)));
boolean scanSucceeded = runCli(ImmutableMap.of(), "--hostname-target=" + HOSTNAME_TARGET);
assertThat(scanSucceeded).isTrue();
TargetInfo targetInfo =
TargetInfo.newBuilder()
.addNetworkEndpoints(NetworkEndpointUtils.forHostname(HOSTNAME_TARGET))
.build();
verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture());
ScanResults storedScanResult = scanResultsCaptor.getValue();
assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED);
assertThat(storedScanResult.getScanFindingsList())
.containsExactlyElementsIn(
Stream.of(
FakeVulnDetector.getFakeDetectionReport(targetInfo, expectedNetworkService),
FakeVulnDetector2.getFakeDetectionReport(targetInfo, expectedNetworkService))
.map(TsunamiCliTest::buildScanFindingFromDetectionReport)
.toArray());
assertThat(storedScanResult.getReconnaissanceReport())
.isEqualTo(
ReconnaissanceReport.newBuilder()
.setTargetInfo(
TargetInfo.newBuilder()
.addNetworkEndpoints(NetworkEndpointUtils.forHostname(HOSTNAME_TARGET)))
.addNetworkServices(
FakeServiceFingerprinter.addWebServiceContext(
FakePortScanner.getFakeNetworkService(
NetworkEndpointUtils.forHostname(HOSTNAME_TARGET))))
.build());
}
@Test
public void run_whenUriTarget_generatesCorrectResult()
throws InterruptedException, ExecutionException, IOException {
boolean scanSucceeded = runCli(ImmutableMap.of(), "--uri-target=" + URI_TARGET);
assertThat(scanSucceeded).isTrue();
URL url = new URL(URI_TARGET);
String hostname = url.getHost();
String ipaddress = InetAddress.getByName(hostname).getHostAddress();
InetAddress inetAddress = InetAddress.getByName(url.getHost());
AddressFamily addressFamily =
inetAddress instanceof Inet4Address ? AddressFamily.IPV4 : AddressFamily.IPV6;
NetworkEndpoint networkEndpoint =
NetworkEndpoint.newBuilder()
.setType(NetworkEndpoint.Type.IP_HOSTNAME_PORT)
.setHostname(Hostname.newBuilder().setName("localhost"))
.setPort(Port.newBuilder().setPortNumber(443))
.setIpAddress(
IpAddress.newBuilder().setAddressFamily(addressFamily).setAddress(ipaddress))
.build();
verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture());
ScanResults storedScanResult = scanResultsCaptor.getValue();
assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED);
assertThat(storedScanResult.getReconnaissanceReport())
.isEqualTo(
ReconnaissanceReport.newBuilder()
.setTargetInfo(TargetInfo.newBuilder().addNetworkEndpoints(networkEndpoint))
.addNetworkServices(
NetworkService.newBuilder()
.setNetworkEndpoint(networkEndpoint)
.setTransportProtocol(TransportProtocol.TCP)
.setServiceName("https")
.setServiceContext(
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder()
.setApplicationRoot(url.getPath()))))
.build());
}
@Test
public void run_whenIpAndHostnameTarget_generatesCorrectResult()
throws InterruptedException, ExecutionException, IOException {
boolean scanSucceeded =
runCli(
ImmutableMap.of(),
"--ip-v4-target=" + IP_TARGET,
"--hostname-target=" + HOSTNAME_TARGET);
assertThat(scanSucceeded).isTrue();
verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture());
ScanResults storedScanResult = scanResultsCaptor.getValue();
assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.SUCCEEDED);
assertThat(storedScanResult.getReconnaissanceReport())
.isEqualTo(
ReconnaissanceReport.newBuilder()
.setTargetInfo(
TargetInfo.newBuilder()
.addNetworkEndpoints(
NetworkEndpointUtils.forIpAndHostname(IP_TARGET, HOSTNAME_TARGET)))
.addNetworkServices(
FakeServiceFingerprinter.addWebServiceContext(
FakePortScanner.getFakeNetworkService(
NetworkEndpointUtils.forIpAndHostname(IP_TARGET, HOSTNAME_TARGET))))
.build());
}
@Test
public void run_whenScanFailed_generatesFailedScanResults()
throws InterruptedException, ExecutionException, IOException {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
bind(ScanResultsArchiver.class).toInstance(scanResultsArchiver);
install(new HttpClientModule.Builder().build());
install(new PayloadGeneratorModule(new SecureRandom()));
install(
new ConfigModule(scanResult, TsunamiConfig.fromYamlData(ImmutableMap.of())));
install(
new CliOptionsModule(
scanResult,
"TsunamiCliTest",
new String[] {
"--ip-v4-target=" + IP_TARGET, "--hostname-target=" + HOSTNAME_TARGET
}));
install(new FakeUtcClockModule());
install(new FakePluginExecutionModule());
install(new FakePortScannerBootstrapModule());
install(new FailedVulnDetectorBootstrapModule());
install(new RemoteServerLoaderModule(ImmutableList.of()));
}
})
.injectMembers(this);
boolean scanSucceeded = tsunamiCli.run();
assertThat(scanSucceeded).isFalse();
verify(scanResultsArchiver, times(1)).archive(scanResultsCaptor.capture());
ScanResults storedScanResult = scanResultsCaptor.getValue();
assertThat(storedScanResult.getScanStatus()).isEqualTo(ScanStatus.FAILED);
assertThat(storedScanResult.getStatusMessage()).isEqualTo("All VulnDetectors failed.");
}
}
@Test
public void run_whenAdvisoryMode_generatesAdvisories()
throws InterruptedException, ExecutionException, ScanningWorkflowException, IOException {
File tempFile = tempFolder.newFile("advisories.csv");
Path tempPath = tempFile.toPath();
boolean scanSucceeded = runCli(ImmutableMap.of(), "--dump-advisories=" + tempPath.toString());
String advisories = Files.readString(tempPath);
String expectedAdvisories =
"""
vulnerabilities {
main_id {
publisher: "GOOGLE"
value: "FakeVuln1"
}
severity: CRITICAL
title: "FakeTitle1"
description: "FakeDescription1"
}
vulnerabilities {
main_id {
publisher: "GOOGLE"
value: "FakeVuln2"
}
severity: MEDIUM
title: "FakeTitle2"
description: "FakeDescription2"
}
""";
assertThat(scanSucceeded).isTrue();
assertThat(advisories).isEqualTo(expectedAdvisories);
}
private static ScanFinding buildScanFindingFromDetectionReport(DetectionReport detectionReport) {
return ScanFinding.newBuilder()
.setTargetInfo(detectionReport.getTargetInfo())
.setNetworkService(detectionReport.getNetworkService())
.setVulnerability(detectionReport.getVulnerability())
.build();
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/option/MainCliOptionsTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link MainCliOptions}. */
@RunWith(JUnit4.class)
public class MainCliOptionsTest {
@Test
public void validate_whenDumpAdvisoriesPathPassed_doesNotThrowParameterException() {
MainCliOptions cliOptions = new MainCliOptions();
cliOptions.dumpAdvisoriesPath = "path/to/dump/advisories";
cliOptions.validate();
}
@Test
public void validate_whenMissingScanTarget_throwsParameterException() {
MainCliOptions cliOptions = new MainCliOptions();
assertThrows(ParameterException.class, cliOptions::validate);
}
@Test
public void validate_whenUriTargetPassedWithHostnameTarget_throwsParameterException() {
MainCliOptions cliOptions = new MainCliOptions();
cliOptions.hostnameTarget = "localhost";
cliOptions.uriTarget = "https://localhost/function1";
assertThrows(ParameterException.class, cliOptions::validate);
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/option/OutputDataFormatTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link OutputDataFormat}. */
@RunWith(JUnit4.class)
public class OutputDataFormatTest {
@Test
public void parse_whenStringMatchesExactly_returnsParsedOutputDataFormat() {
assertThat(OutputDataFormat.parse("BIN_PROTO")).hasValue(OutputDataFormat.BIN_PROTO);
assertThat(OutputDataFormat.parse("JSON")).hasValue(OutputDataFormat.JSON);
}
@Test
public void parse_whenStringMatchesIgnoringCases_returnsParsedOutputDataFormat() {
assertThat(OutputDataFormat.parse("bin_proto")).hasValue(OutputDataFormat.BIN_PROTO);
assertThat(OutputDataFormat.parse("Json")).hasValue(OutputDataFormat.JSON);
}
@Test
public void parse_whenStringNotMatch_returnsEmpty() {
assertThat(OutputDataFormat.parse("xml")).isEmpty();
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpV4ValidatorTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import com.google.common.collect.ImmutableList;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link IpV4Validator}. */
@RunWith(JUnit4.class)
public class IpV4ValidatorTest extends IpValidatorTest {
@Override
protected String flagName() {
return "ip-v4-target";
}
@Override
protected IpValidator getValidator() {
return new IpV4Validator();
}
@Override
protected ImmutableList validIps() {
return ImmutableList.of("127.0.0.1", "8.8.8.8");
}
@Override
protected ImmutableList invalidIps() {
return ImmutableList.of("", "bogus_string", "1234", "2002:af4:9b91::", "www.google.com");
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpV6ValidatorTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import com.google.common.collect.ImmutableList;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link IpV6Validator}. */
@RunWith(JUnit4.class)
public class IpV6ValidatorTest extends IpValidatorTest {
@Override
protected String flagName() {
return "ip-v6-target";
}
@Override
protected IpValidator getValidator() {
return new IpV6Validator();
}
@Override
protected ImmutableList validIps() {
return ImmutableList.of(
"0:0:0:0:0:0:0:1",
"fe80::a",
"fe80::1",
"fe80::2",
"fe80::42",
"fe80::3dd0:7f8e:57b7:34d5",
"fe80:3dd0:7f8e:57b7:0:0:0:0",
"::4:0:0:0:ffff",
"0:0:3::ffff",
"7::0.128.0.127");
}
@Override
protected ImmutableList invalidIps() {
return ImmutableList.of(
"", "bogus_string", "1234", "127.0.0.1", "www.google.com", "[1:2e]", "[fe80:a");
}
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/option/validator/IpValidatorTest.java
================================================
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.option.validator;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
/** Base class for IP validators. */
public abstract class IpValidatorTest {
@Test
public void validate_withValidIpValue_doesNotThrows() {
for (String validIp : validIps()) {
try {
getValidator().validate(flagName(), validIp);
} catch (ParameterException e) {
throw new AssertionError("Unexpected ParameterException for IP: " + validIp, e);
}
}
}
@Test
public void validate_withInvalidIpValue_throwsParameterException() {
for (String invalidIp : invalidIps()) {
ParameterException exception =
assertThrows(
ParameterException.class, () -> getValidator().validate(flagName(), invalidIp));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Parameter %s should point to a valid IP v%d address, got '%s'",
flagName(), getValidator().ipVersion(), invalidIp));
}
}
protected abstract String flagName();
protected abstract IpValidator getValidator();
protected abstract ImmutableList validIps();
protected abstract ImmutableList invalidIps();
}
================================================
FILE: main/src/test/java/com/google/tsunami/main/cli/server/RemoteServerLoaderTest.java
================================================
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.tsunami.main.cli.server;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.inject.Guice;
import com.google.tsunami.common.server.LanguageServerCommand;
import java.time.Duration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class RemoteServerLoaderTest {
@Test
public void runServerProcess_whenPathExistsAndNormalPort_returnsValidProcessList() {
ImmutableList