Web Service#
REST API service for anonymizing iCalendar files. Multiple endpoints for different input methods, plus shareable links for easy collaboration.
Hosted Service#
A public instance is available at https://icalendar-anonymizer.com - no installation required.
Features of the hosted service:
Shareable links: Generate URLs to share anonymized calendars (30-day expiry)
R2 storage: Calendars stored on Cloudflare R2 for reliable access
Global edge network: Fast response times worldwide via Cloudflare Workers
No account required: Use immediately without signup
For self-hosting options, see the Self-Hosting guide.
Installation#
Install the web service dependencies:
pip install icalendar-anonymizer[web]
This installs:
fastapi>=0.128.0- Web frameworkuvicorn>=0.38.0- ASGI serverpython-multipart>=0.0.18- File upload supporthttpx>=0.28.1- Async HTTP client for URL fetchingcryptography>=46.0.0- Fernet encryption for shareable links
Running the Server#
Start the server with uvicorn:
uvicorn icalendar_anonymizer.webapp.main:app --reload
The server starts on http://127.0.0.1:8000 by default.
For production deployment:
uvicorn icalendar_anonymizer.webapp.main:app --host 0.0.0.0 --port 8000
API Documentation#
Interactive API documentation is available at:
Swagger UI: http://127.0.0.1:8000/docs
ReDoc: http://127.0.0.1:8000/redoc
Frontend Interface#
The web service includes a user-friendly web interface at the root URL (/).
Features#
The frontend provides three input methods:
- Upload File
Drag and drop or click to select an
.icsfile from your device. Maximum file size: 10 MB.- Paste Content
Copy and paste iCalendar content directly into a text area. Useful for small calendars or quick tests.
- Fetch from URL
Enter a URL to fetch and anonymize a remote calendar. Subject to SSRF protection (see security considerations).
Advanced Options#
All three input methods include an “Advanced Options” collapsible section for granular field control.
Configure how each field is anonymized:
keep - Preserve original value
remove - Strip property entirely
randomize (default) - Hash to deterministic random value
replace - Replace with fixed placeholder
Configurable fields (10 total):
Summary, Description, Location
Comment, Contact, Resources, Categories
Attendee, Organizer, UID
The UID field cannot use remove mode (would break recurring events).
Accessibility#
The interface is fully accessible:
Keyboard navigation: All controls keyboard accessible with proper tab order
Screen reader support: ARIA labels and live regions for status updates
High contrast: WCAG AA compliant color contrast ratios
Mobile responsive: Works on all device sizes
Progressive enhancement: Basic functionality works without JavaScript
Usage Example#
Navigate to
http://localhost:8000/Choose an input method (upload, paste, or fetch)
Submit your calendar
Click “Download” to save the anonymized result
The interface handles errors gracefully with informative messages.
No-JavaScript Fallback#
If JavaScript is disabled, file upload still works via direct form submission to the
/upload endpoint. The file downloads automatically on success.
API Endpoints#
GET/POST /anonymized#
curl-friendly endpoint for scripting and testing. Returns raw ICS without JSON wrapper.
GET with query parameter
GET /anonymized?ics=BEGIN:VCALENDAR... HTTP/1.1
POST with raw body
POST /anonymized HTTP/1.1
Content-Type: text/plain
BEGIN:VCALENDAR
VERSION:2.0
...
Response (200 OK)
HTTP/1.1 200 OK
Content-Type: text/calendar; charset=utf-8
BEGIN:VCALENDAR
VERSION:2.0
...
No Content-Disposition header, allowing direct piping to files.
Error Responses
400 Bad Request- Missingicsparameter (GET), empty body (POST), invalid UTF-8, or invalid ICS format
Examples with curl
# POST with file (primary use case)
curl -X POST --data-binary @calendar.ics https://icalendar-anonymizer.com/anonymized
# Pipe to output file
curl -X POST --data-binary @calendar.ics https://icalendar-anonymizer.com/anonymized > anonymized.ics
# Pipe from stdin
cat calendar.ics | curl -X POST --data-binary @- https://icalendar-anonymizer.com/anonymized
# GET with small test calendar (URL-encoded)
curl "https://icalendar-anonymizer.com/anonymized?ics=BEGIN:VCALENDAR%0AVERSION:2.0%0AEND:VCALENDAR"
Note
GET requests have URL length limits (~2KB). Use POST for anything beyond tiny test calendars.
POST /anonymize#
Anonymize iCalendar content provided as JSON. Optionally configure per-field anonymization.
Request
POST /anonymize HTTP/1.1
Content-Type: application/json
{
"ics": "BEGIN:VCALENDAR\nVERSION:2.0\n...",
"config": {
"summary": "keep",
"location": "remove",
"description": "replace"
}
}
The config field is optional. If omitted, all fields use default randomize behavior.
Response (200 OK)
HTTP/1.1 200 OK
Content-Type: text/calendar
Content-Disposition: attachment; filename="anonymized.ics"
BEGIN:VCALENDAR
VERSION:2.0
...
Error Responses
400 Bad Request- Invalid ICS format or empty input422 Unprocessable Entity- Invalid field config (invalid field name, invalid mode, or UID set to remove)500 Internal Server Error- Anonymization failed
Example with curl
# Basic anonymization
curl -X POST http://localhost:8000/anonymize \
-H "Content-Type: application/json" \
-d '{"ics": "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR"}' \
-o anonymized.ics
# With field configuration
curl -X POST http://localhost:8000/anonymize \
-H "Content-Type: application/json" \
-d '{"ics": "BEGIN:VCALENDAR\n...", "config": {"summary": "keep", "location": "remove"}}' \
-o anonymized.ics
POST /upload#
Anonymize an uploaded iCalendar file. Optionally configure per-field anonymization.
Request
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="calendar.ics"
Content-Type: text/calendar
BEGIN:VCALENDAR
VERSION:2.0
...
------WebKitFormBoundary
Content-Disposition: form-data; name="config"
{"summary": "keep", "location": "remove"}
The config field is optional JSON string. If omitted, all fields use default randomize behavior.
Response (200 OK)
HTTP/1.1 200 OK
Content-Type: text/calendar
Content-Disposition: attachment; filename="anonymized.ics"
BEGIN:VCALENDAR
VERSION:2.0
...
Error Responses
400 Bad Request- Invalid ICS format, empty file, non-UTF-8 encoding, or invalid config JSON413 Payload Too Large- File exceeds size limit422 Unprocessable Entity- Invalid field config500 Internal Server Error- Anonymization failed
Example with curl
# Basic upload
curl -X POST http://localhost:8000/upload \
-F "file=@calendar.ics" \
-o anonymized.ics
# With field configuration
curl -X POST http://localhost:8000/upload \
-F "file=@calendar.ics" \
-F 'config={"summary": "keep", "location": "remove"}' \
-o anonymized.ics
GET /fetch#
Fetch an iCalendar file from a URL and anonymize it. Optionally configure per-field anonymization via query parameters.
Security Features
This endpoint includes SSRF (Server-Side Request Forgery) protection:
Blocks private IP ranges (10.x, 172.16.x, 192.168.x, 169.254.x)
Blocks localhost (127.0.0.1, ::1, 0.0.0.0)
Blocks IPv6 private ranges (fc00::/7, fe80::/10)
10-second timeout
10 MB size limit
Validates redirect destinations
Request
GET /fetch?url=https://example.com/calendar.ics&summary=keep&location=remove HTTP/1.1
Field configuration parameters (all optional):
summary,description,location,commentcontact,resources,categoriesattendee,organizer,uid
Each accepts: keep, remove, randomize, replace
Response (200 OK)
HTTP/1.1 200 OK
Content-Type: text/calendar
Content-Disposition: attachment; filename="anonymized.ics"
BEGIN:VCALENDAR
VERSION:2.0
...
Error Responses
400 Bad Request- Invalid URL, private IP, invalid ICS format, or connection failed408 Request Timeout- Request exceeded 10-second timeout413 Payload Too Large- Response exceeds 10 MB size limit422 Unprocessable Entity- Invalid field configVarious HTTP status codes- Returns the actual HTTP status code from the upstream server (e.g., 404 Not Found, 500 Internal Server Error, 503 Service Unavailable)500 Internal Server Error- Anonymization failed
Example with curl
# Basic fetch
curl "http://localhost:8000/fetch?url=https://example.com/calendar.ics" \
-o anonymized.ics
# With field configuration
curl "http://localhost:8000/fetch?url=https://example.com/calendar.ics&summary=keep&location=remove" \
-o anonymized.ics
Known Limitations
The SSRF protection has a Time-of-Check-Time-of-Use (TOCTOU) vulnerability to DNS rebinding attacks. See Issue #70 for future enhancements.
POST /fernet-generate#
Generate an encrypted Fernet token for live calendar proxying. Only available when FERNET_KEY environment variable is set.
Request
POST /fernet-generate HTTP/1.1
Content-Type: application/json
{
"url": "https://example.com/calendar.ics"
}
Response (200 OK)
{
"url": "https://icalendar-anonymizer.com/fernet/gAAAAABl..."
}
The returned URL contains an encrypted token with the source calendar URL and a random salt. Anyone with this URL can fetch the calendar, which will be fetched from the source and anonymized on-the-fly.
Error Responses
400 Bad Request- Invalid URL scheme, localhost, or private IP503 Service Unavailable- Fernet not configured (FERNET_KEYnot set)
Example with curl
curl -X POST http://localhost:8000/fernet-generate \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/calendar.ics"}'
# Response: {"url":"http://localhost:8000/fernet/gAAAAABl..."}
Security Features
Source URL validated for SSRF protection (same rules as
/fetch)Token encrypted with Fernet symmetric encryption
Unique random salt per token ensures different anonymization
Token contains authenticated data preventing tampering
GET /fernet/{token}#
Fetch and anonymize a calendar using an encrypted Fernet token.
Request
GET /fernet/gAAAAABl... HTTP/1.1
Response (200 OK)
HTTP/1.1 200 OK
Content-Type: text/calendar
Content-Disposition: attachment; filename="anonymized.ics"
Cache-Control: no-cache
BEGIN:VCALENDAR
VERSION:2.0
...
Error Responses
400 Bad Request- Invalid token, malformed payload, missing URL/salt, or invalid ICS format408 Request Timeout- Source calendar fetch exceeded 10-second timeout413 Payload Too Large- Source calendar exceeds 10 MB size limit503 Service Unavailable- Fernet not configured (FERNET_KEYnot set)Various HTTP status codes- Returns the actual HTTP status code from the upstream server
Example with curl
curl "http://localhost:8000/fernet/gAAAAABl..." -o anonymized.ics
How It Works
Token is decrypted to retrieve source URL and salt
Source URL is validated for SSRF protection
Calendar is fetched from source (with redirect validation)
Calendar is anonymized using the salt from token
Anonymized calendar is returned
This provides live proxying - the source is fetched each time, so the anonymized calendar stays up-to-date.
GET /health#
Health check endpoint for monitoring.
Response (200 OK)
{
"status": "healthy",
"version": "0.2.0",
"r2_enabled": true,
"fernet_enabled": false
}
Fields
statusAlways
"healthy"(endpoint returns 200 only when service is operational)versionPackage version string
r2_enabledWhether R2 static snapshot shareable links are available (requires Cloudflare Workers environment)
fernet_enabledWhether Fernet live proxy shareable links are available (requires
FERNET_KEYenvironment variable)
Error Responses#
All error responses return JSON with the following format:
{
"detail": "Error message describing what went wrong"
}
Common error scenarios:
Invalid ICS Format
{
"detail": "Invalid ICS format: Expected BEGIN:VCALENDAR"
}
Empty Input
{
"detail": "Input is empty"
}
Private IP Blocked
{
"detail": "Access to private IP 192.168.1.1 is not allowed"
}
URL Fetch Failed
{
"detail": "Failed to fetch URL: Connection timeout"
}
CORS Configuration#
The server enables CORS with wildcard origins for development:
allow_origins=["*"]
allow_credentials=False
allow_methods=["*"]
allow_headers=["*"]
Production Hardening
For production deployments, configure CORS to only allow your frontend domain:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["POST", "GET"],
allow_headers=["Content-Type"],
)
Self-Hosting#
Docker Deployment#
Pull and run the Docker image:
docker pull sashankbhamidi/icalendar-anonymizer
docker run -p 8000:8000 sashankbhamidi/icalendar-anonymizer
The web service will be available at http://localhost:8000.
Build from source:
docker build -t icalendar-anonymizer .
docker run -p 8000:8000 icalendar-anonymizer
Manual Deployment#
For production deployment on a VPS or cloud server:
Install Python 3.11 or later
Create a virtual environment:
python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate
Install the package:
pip install icalendar-anonymizer[web]
Run with production settings:
uvicorn icalendar_anonymizer.webapp.main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 4 \ --log-level info
Use a reverse proxy (nginx/Apache) for HTTPS and load balancing
Systemd Service#
Create /etc/systemd/system/icalendar-anonymizer.service:
[Unit]
Description=iCalendar Anonymizer Web Service
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/opt/icalendar-anonymizer
Environment="PATH=/opt/icalendar-anonymizer/venv/bin"
ExecStart=/opt/icalendar-anonymizer/venv/bin/uvicorn \
icalendar_anonymizer.webapp.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable icalendar-anonymizer
sudo systemctl start icalendar-anonymizer
Nginx Reverse Proxy#
Example nginx configuration:
server {
listen 80;
server_name anonymizer.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Add SSL with Let’s Encrypt:
sudo certbot --nginx -d anonymizer.example.com
Security Considerations#
SSRF Protection
The /fetch endpoint implements SSRF protection but has known limitations.
For high-security deployments:
Use network-level firewall rules
Deploy in an isolated network segment
Implement additional rate limiting
Monitor for suspicious URL patterns
See Issue #70 for planned enhancements.
Input Validation
All endpoints validate:
UTF-8 encoding (no binary corruption)
iCalendar format (BEGIN:VCALENDAR required)
File size limits (10 MB for URL fetching)
Error Disclosure
Error messages include technical details to aid debugging. For production, consider customizing error handlers to limit information disclosure.
Testing#
Run the test suite:
pip install -e ".[test,web]"
pytest src/icalendar_anonymizer/tests/web/
Test coverage includes:
All three endpoints with valid and invalid inputs
SSRF protection (private IPs, localhost, redirects)
UTF-8 encoding validation
Error handling scenarios
Large file handling
Performance#
Benchmarks
Approximate performance on a modern server:
JSON input (
/anonymize): ~50ms for typical calendarFile upload (
/upload): ~60ms including multipart parsingURL fetch (
/fetch): ~200ms including network latency
Scaling
For high-traffic deployments:
Increase uvicorn workers:
--workers 8Use multiple server instances behind a load balancer
Consider async worker pools for URL fetching
Implement caching for frequently accessed URLs (see Issue #30)
Troubleshooting#
ImportError: No module named ‘fastapi’
Install the web extras:
pip install icalendar-anonymizer[web]
Connection Refused
Check if the server is running:
curl http://localhost:8000/docs
If not, start it:
uvicorn icalendar_anonymizer.webapp.main:app --reload
CORS Errors in Browser
The server allows all origins by default. If you’re seeing CORS errors, check that your frontend is making requests to the correct URL.
Timeout on /fetch
The endpoint has a 10-second timeout. For slow servers, the request will fail with a timeout error. This is intentional to prevent resource exhaustion.
See Also#
Python API - Using the Python library directly
Command-Line Interface - Command-line interface
Contributing - Development guide