Pipeline shows disabled endpoints fix (#2881) (#3282)

# Description of Changes

Previously, the dropdown menu in the pipeline configuration displayed
all endpoints, including disabled ones, and allowed API calls to them.

Changes:

- Updated EndpointInterceptor to correctly parse request URIs and match
them to corresponding endpoint names in settings.yml, ensuring disabled
endpoints are blocked.

- Added a new API endpoint in SettingsController to expose the
endpointStatus map, allowing the frontend to check which endpoints are
disabled.

- Updated pipeline.js to use this new API and hide disabled endpoints
from the dropdown menu.

Tests:

- Created a new Docker Compose setup using a custom settings.yml where
all endpoints are disabled.

- Implemented a test script to run this setup, send API requests to
disabled endpoints, and verify they are correctly blocked.

[Bug Fix Video](https://youtu.be/L1z3jZh8z8E)

Closes #2881

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
André Santos 2025-04-09 11:03:40 +01:00 committed by GitHub
parent 4e63a684b5
commit 7f8e3d676d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 501 additions and 8 deletions

View File

@ -141,4 +141,5 @@ jobs:
run: |
chmod +x ./testing/test_webpages.sh
chmod +x ./testing/test.sh
chmod +x ./testing/test_disabledEndpoints.sh
./testing/test.sh

View File

@ -0,0 +1,35 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Fat-Disable-Endpoints
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
- ../testing/allEndpointsRemovedSettings.yml:/configs/settings.yml:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with all Endpoints Disabled
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@ -45,6 +45,10 @@ public class EndpointConfiguration {
}
}
public Map<String, Boolean> getEndpointStatuses() {
return endpointStatuses;
}
public boolean isEndpointEnabled(String endpoint) {
if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1);

View File

@ -23,7 +23,29 @@ public class EndpointInterceptor implements HandlerInterceptor {
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
boolean isEnabled;
// Extract the specific endpoint name (e.g: /api/v1/general/remove-pages -> remove-pages)
if (requestURI.contains("/api/v1") && requestURI.split("/").length > 4) {
String[] requestURIParts = requestURI.split("/");
String requestEndpoint;
// Endpoint: /api/v1/convert/pdf/img becomes pdf-to-img
if ("convert".equals(requestURIParts[3]) && requestURIParts.length > 5) {
requestEndpoint = requestURIParts[4] + "-to-" + requestURIParts[5];
} else {
requestEndpoint = requestURIParts[4];
}
log.debug("Request endpoint: {}", requestEndpoint);
isEnabled = endpointConfiguration.isEndpointEnabled(requestEndpoint);
log.debug("Is endpoint enabled: {}", isEnabled);
} else {
isEnabled = endpointConfiguration.isEndpointEnabled(requestURI);
}
if (!isEnabled) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}

View File

@ -1,10 +1,12 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@ -23,9 +26,13 @@ import stirling.software.SPDF.utils.GeneralUtils;
public class SettingsController {
private final ApplicationProperties applicationProperties;
private final EndpointConfiguration endpointConfiguration;
public SettingsController(ApplicationProperties applicationProperties) {
public SettingsController(
ApplicationProperties applicationProperties,
EndpointConfiguration endpointConfiguration) {
this.applicationProperties = applicationProperties;
this.endpointConfiguration = endpointConfiguration;
}
@PostMapping("/update-enable-analytics")
@ -41,4 +48,10 @@ public class SettingsController {
applicationProperties.getSystem().setEnableAnalytics(enabled);
return ResponseEntity.ok("Updated");
}
@GetMapping("/get-endpoints-status")
@Hidden
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
}
}

View File

@ -153,22 +153,32 @@ document.getElementById("submitConfigBtn").addEventListener("click", function ()
let apiDocs = {};
let apiSchemas = {};
let operationSettings = {};
let operationStatus = {};
fetchWithCsrf("v1/api-docs")
.then((response) => response.json())
.then((data) => {
apiDocs = data.paths;
apiSchemas = data.components.schemas;
let operationsDropdown = document.getElementById("operationsDropdown");
return fetchWithCsrf("api/v1/settings/get-endpoints-status")
.then((response) => response.json())
.then((data) => {
operationStatus = data;
})
.catch((error) => {
console.error("Error:", error);
});
})
.then(() => {
const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here
let operationsDropdown = document.getElementById("operationsDropdown");
operationsDropdown.innerHTML = "";
let operationsByTag = {};
// Group operations by tags
Object.keys(data.paths).forEach((operationPath) => {
let operation = data.paths[operationPath].post;
Object.keys(apiDocs).forEach((operationPath) => {
let operation = apiDocs[operationPath].post;
if (!operation || !operation.description) {
console.log(operationPath);
}
@ -209,13 +219,19 @@ fetchWithCsrf("v1/api-docs")
}
operationPathDisplay = operationPathDisplay.replaceAll(" ", "-");
option.textContent = operationPathDisplay;
if (!(operationPathDisplay in operationStatus)) {
option.value = operationPath; // Keep the value with slashes for querying
group.appendChild(option);
}
});
operationsDropdown.appendChild(group);
}
});
})
.catch((error) => {
console.error("Error:", error);
});
document.getElementById('deletePipelineBtn').addEventListener('click', function(event) {

View File

@ -0,0 +1,147 @@
#############################################################################################################
# Welcome to settings file from #
# ____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____ #
# / ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___| #
# \___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_ #
# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| #
# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| #
# #
# Custom setting.yml file with all endpoints disabled to only be used for testing purposes #
# Do not comment out any entry, it will be removed on next startup #
# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME #
#############################################################################################################
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
initialLogin:
username: '' # initial username for the first login
password: '' # initial password for the first login
oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client:
keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2. Available options are: [email | name | given_name | family_name | preferred_name]
google:
clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2
scopes: email, profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2. Available options are: [email | name | given_name | family_name]
github:
clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2. Available options are: [email | login | name]
issuer: '' # set to any Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your Provider
clientSecret: '' # client secret from your Provider
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
provider: '' # The name of your Provider
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
idpIssuer: '' # The ID of your Provider
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
SSOAutoLogin: false # Enable to auto login to first provided SSO
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
system:
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: true # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
datasource:
enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration
customDatabaseUrl: '' # eg jdbc:postgresql://localhost:5432/postgres, set the url for your own custom database connection. If provided, the type, hostName, port and name are not necessary and will not be used
username: postgres # set the database username
password: postgres # set the database password
type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql')
hostName: localhost # the host name to use for the database url. Set to 'localhost' when running the app locally. Set to match the name of the container name of your database container when running the app on a server (Docker configuration)
port: 5432 # set the port number of the database. Ensure this matches the port the database is listening to
name: postgres # set the name of your database. Should match the name of the database you create
customPaths:
pipeline:
watchedFoldersDir: "" #Defaults to /pipeline/watchedFolders
finishedFoldersDir: "" #Defaults to /pipeline/finishedFolders
operations:
weasyprint: "" #Defaults to /opt/venv/bin/weasyprint
unoconvert: "" #Defaults to /opt/venv/bin/unoconvert
ui:
appName: '' # application's visible name
homeDescription: '' # short description or tagline shown on the homepage
appNameNavbar: '' # name displayed on the navigation bar
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
endpoints: # All the possible endpoints are disabled
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
appVersion: 0.44.3
processExecutor:
sessionLimit: # Process executor instances limits
libreOfficeSessionLimit: 1
pdfToHtmlSessionLimit: 1
qpdfSessionLimit: 4
tesseractSessionLimit: 1
pythonOpenCvSessionLimit: 8
weasyPrintSessionLimit: 16
installAppSessionLimit: 1
calibreSessionLimit: 1
timeoutMinutes: # Process executor timeout in minutes
libreOfficetimeoutMinutes: 30
pdfToHtmltimeoutMinutes: 20
pythonOpenCvtimeoutMinutes: 30
weasyPrinttimeoutMinutes: 30
installApptimeoutMinutes: 60
calibretimeoutMinutes: 30
tesseractTimeoutMinutes: 30

60
testing/endpoints.txt Normal file
View File

@ -0,0 +1,60 @@
/api/v1/filter/filter-page-size
/api/v1/filter/filter-page-rotation
/api/v1/filter/filter-page-count
/api/v1/filter/filter-file-size
/api/v1/filter/filter-contains-text
/api/v1/filter/filter-contains-image
/api/v1/security/validate-signature
/api/v1/security/sanitize-pdf
/api/v1/security/remove-password
/api/v1/security/remove-cert-sign
/api/v1/security/redact
/api/v1/security/get-info-on-pdf
/api/v1/security/cert-sign
/api/v1/security/auto-redact
/api/v1/security/add-watermark
/api/v1/security/add-password
/api/v1/misc/update-metadata
/api/v1/misc/show-javascript
/api/v1/misc/replace-invert-pdf
/api/v1/misc/repair
/api/v1/misc/remove-blanks
/api/v1/misc/ocr-pdf
/api/v1/misc/flatten
/api/v1/misc/extract-images
/api/v1/misc/extract-image-scans
/api/v1/misc/decompress-pdf
/api/v1/misc/compress-pdf
/api/v1/misc/auto-split-pdf
/api/v1/misc/auto-rename
/api/v1/misc/add-stamp
/api/v1/misc/add-page-numbers
/api/v1/misc/add-image
/api/v1/convert/url/pdf
/api/v1/convert/pdf/xml
/api/v1/convert/pdf/word
/api/v1/convert/pdf/text
/api/v1/convert/pdf/presentation
/api/v1/convert/pdf/pdfa
/api/v1/convert/pdf/markdown
/api/v1/convert/pdf/img
/api/v1/convert/pdf/html
/api/v1/convert/pdf/csv
/api/v1/convert/markdown/pdf
/api/v1/convert/img/pdf
/api/v1/convert/html/pdf
/api/v1/convert/file/pdf
/api/v1/general/split-pdf-by-sections
/api/v1/general/split-pdf-by-chapters
/api/v1/general/split-pages
/api/v1/general/split-by-size-or-count
/api/v1/general/scale-pages
/api/v1/general/rotate-pdf
/api/v1/general/remove-pages
/api/v1/general/remove-image-pdf
/api/v1/general/rearrange-pages
/api/v1/general/pdf-to-single-page
/api/v1/general/overlay-pdfs
/api/v1/general/multi-page-layout
/api/v1/general/merge-pdfs
/api/v1/general/crop

View File

@ -332,6 +332,18 @@ main() {
docker-compose -f "./exampleYmlFiles/test_cicd.yml" down
run_tests "Stirling-PDF-Fat-Disable-Endpoints" "./exampleYmlFiles/docker-compose-latest-fat-endpoints-disabled.yml"
echo "Testing disabled endpoints..."
if ./testing/test_disabledEndpoints.sh -f ./testing/endpoints.txt -b http://localhost:8080; then
passed_tests+=("Disabled-Endpoints")
else
failed_tests+=("Disabled-Endpoints")
echo "Disabled Endpoints tests failed"
fi
docker-compose -f "./exampleYmlFiles/docker-compose-latest-fat-endpoints-disabled.yml" down
# Report results
echo "All tests completed in $SECONDS seconds."

View File

@ -0,0 +1,183 @@
#!/bin/bash
# Function to check a single endpoint
check_endpoint() {
local endpoint=$(echo "$1" | tr -d '\r') # Remove carriage returns
local base_url=$(echo "$2" | tr -d '\r')
local full_url="${base_url}${endpoint}"
local timeout=10
local result_file="$3"
local api_key="$4"
# Use curl to fetch the endpoint with timeout
response=$(curl -s -w "\n%{http_code}" --max-time $timeout \
-H "accept: */*" \
-H "Content-Type: multipart/form-data" \
-F "additional_field=" \
"$full_url")
if [ $? -ne 0 ]; then
echo "FAILED - Connection error or timeout $full_url" >> "$result_file"
return 1
fi
# Split response into body and status code
HTTP_STATUS=$(echo "$response" | tail -n1)
BODY=$(echo "$response" | sed '$d')
# Check HTTP status
if [ "$HTTP_STATUS" != "403" ]; then
echo "FAILED - HTTP Status: $HTTP_STATUS - $full_url" >> "$result_file"
return 1
fi
echo "OK - $full_url" >> "$result_file"
return 0
}
# Function to test an endpoint and update counters
test_endpoint() {
local endpoint="$1"
local base_url="$2"
local tmp_dir="$3"
local endpoint_index="$4"
local api_key="$5"
local result_file="${tmp_dir}/result_${endpoint_index}.txt"
if ! check_endpoint "$endpoint" "$base_url" "$result_file" "$api_key"; then
echo "1" > "${tmp_dir}/failed_${endpoint_index}"
else
echo "0" > "${tmp_dir}/failed_${endpoint_index}"
fi
}
# Main function to test all endpoints from the list in parallel
test_all_endpoints() {
local endpoint_file="$1"
local base_url="${2:-"http://localhost:8080"}"
local api_key="$3"
local max_parallel="${4:-10}" # Default to 10 parallel processes
local failed_count=0
local total_count=0
local start_time=$(date +%s)
local tmp_dir=$(mktemp -d)
local active_jobs=0
local endpoint_index=0
echo "Starting endpoint tests..."
echo "Base URL: $base_url"
echo "Number of lines: $(wc -l < "$endpoint_file")"
echo "Max parallel jobs: $max_parallel"
echo "----------------------------------------"
# Process each endpoint
while IFS= read -r endpoint || [ -n "$endpoint" ]; do
# Skip empty lines and comments
[[ -z "$endpoint" || "$endpoint" =~ ^#.*$ ]] && continue
((total_count++))
((endpoint_index++))
# Run the check in background
test_endpoint "$endpoint" "$base_url" "$tmp_dir" "$endpoint_index" "$api_key" &
# Track the job
((active_jobs++))
# If we've reached max_parallel, wait for a job to finish
if [ $active_jobs -ge $max_parallel ]; then
wait -n # Wait for any child process to exit
((active_jobs--))
fi
done < "$endpoint_file"
# Wait for remaining jobs to finish
wait
# Print results in order and count failures
for i in $(seq 1 $endpoint_index); do
if [ -f "${tmp_dir}/result_${i}.txt" ]; then
cat "${tmp_dir}/result_${i}.txt"
fi
if [ -f "${tmp_dir}/failed_${i}" ]; then
failed_count=$((failed_count + $(cat "${tmp_dir}/failed_${i}")))
fi
done
# Clean up
rm -rf "$tmp_dir"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
echo "----------------------------------------"
echo "Test Summary:"
echo "Total tests: $total_count"
echo "Failed tests: $failed_count"
echo "Passed tests: $((total_count - failed_count))"
echo "Duration: ${duration} seconds"
return $failed_count
}
# Print usage information
usage() {
echo "Usage: $0 [-f endpoint_file] [-b base_url] [-k api_key] [-p max_parallel]"
echo "Options:"
echo " -f endpoint_file Path to file containing endpoints to test (required)"
echo " -b base_url Base URL to prepend to test endpoints (default: http://localhost:8080)"
echo " -k api_key API key to use for authentication (required)"
echo " -p max_parallel Maximum number of parallel requests (default: 10)"
exit 1
}
# Main execution
main() {
local endpoint_file=""
local base_url="http://localhost:8080"
local api_key="123456789"
local max_parallel=10
# Parse command line options
while getopts ":f:b:h" opt; do
case $opt in
f) endpoint_file="$OPTARG" ;;
b) base_url="$OPTARG" ;;
h) usage ;;
\?) echo "Invalid option -$OPTARG" >&2; usage ;;
esac
done
# Check if endpoint file is provided
if [ -z "$endpoint_file" ]; then
echo "Error: Endpoint file is required"
usage
fi
# Check if endpoint file exists
if [ ! -f "$endpoint_file" ]; then
echo "Error: Endpoint list file not found: $endpoint_file"
exit 1
fi
# Check if API key is provided
if [ -z "$api_key" ]; then
echo "Error: API key is required"
usage
fi
# Run tests using the endpoint list
if test_all_endpoints "$endpoint_file" "$base_url" "$api_key" "$max_parallel"; then
echo "All endpoint tests passed!"
exit 0
else
echo "Some endpoint tests failed!"
exit 1
fi
}
# Run main if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi