Skip to content

Improved OpenAlias API WebForm, with a sleek, modern look

License

Notifications You must be signed in to change notification settings

saloniamatteo/openalias

Repository files navigation

OpenAlias web portal to easily retrieve OpenAlias records of any domain.

This website is built in PHP with the following:

The following are used for the front-end:

Donate

Support this project: salonia.it/donate

Screenshots

Landing page: oa

Page with results: oa-records

Page with no results: oa-norecords

Features

DNSSEC validation

Each requested domain is checked for presence of RRSIG records, which confirm DNSSEC validity.

The validation is done with the following command: host -t RRSIG <domain>, where <domain> corresponds to the requested domain, already sanitized.

The results are then displayed on top of the records table, showing a white check mark on a green background (✅), or a white cross on a red background (❎), followed, respectively, by DNSSEC OK, or DNSSEC FAIL.

AbuseIPDB

This Middleware, written by me, checks if the incoming IP address comes from a "bad" server (crawlers, scanners, etc.), thanks to AbuseIPDB's /check API endpoint.

When a request is received, the BlockRequest Middleware will check the cache, using the incoming IP as key. If a record is found, check if it is a good IP: if it is, proceed with the request. If it isn't, throw a 403, which will be rendered with a pretty page, regardless.

If no records are found in the cache, BlockRequest queries AbuseIPDB, honoring the user-provided options (see below). If the IP address is whitelisted, check if the user wants to ignore this whitelist; we then check the IP score and, if it is above a certain threshold, the request will be blocked, like the case above, throwing a 403.

To use this, create an account, then head over to AbuseIPDB/api and create an APIv2 key. Save this key into the .env file:

# If you want to block incoming requests from bad servers
# using AbuseIPDB, enter your API key here.
ABUSEIPDB_KEY= # Your API key goes here!

You're all set! Make sure the cache store is also properly configured. The cache store provided with this site is file, so you should be good.

Additionally, you can tune the following parameters:

  • ABUSEIPDB_THRESHOLD: The minimum percentage score required for an IP to be considered malicious. Default: 35.
  • ABUSEIPDB_IGNORE_WHITELIST: Ignore AbuseIPDB's whitelist preference for every IP. Default: 0.
  • ABUSEIPDB_CACHE_TTL: Store the results in cache for x minutes. Default: 15
  • ABUSEIPDB_IP_OK: Store this string for a known good IP. Default: OK
  • ABUSEIPDB_IP_BAD: Store this string for a known bad IP. Default: BAD

Rate limiter

Apart from the AbuseIPDB integration, this website uses Laravel's rate limiter. It uses the same CACHE_STORE driver as the AbuseIPDB integration, which defaults to file.

The rate limiter is defined in app/Providers/AppServiceProvider.php as follows:

/* Bootstrap any application services. */
public function boot()
{
	// Limit to 5 requests per minute.
	RateLimiter::for('global', function (Request $request) {
		if (config('APP_ENV') == 'production') {
			return Limit::perMinute(5)->by($request->ip());
		}
	});
}

It is configured to allow a maximum of 5 page requests per minute, before throwing an HTTP 429 (Too many requests).

Asset bundling

Assets are bundled and handled by Vite:

  • CSS & JS files are minified (PostCSS and PurgeCSS) and versioned
  • Images are versioned

This helps with removing unused code, lowering asset size, and lowering page load times.

Run npm run build to re-generate the asset bundle.

Components

Most HTML components (Card, Hero, Tile, etc.) are split up in several files, under resources/views/components/. This makes it way easier and faster to write new pages, thanks to Blade Templates.

Caching

Config, events, views, and routes are cached, making site load-times faster.

Run composer cache to cache them.

Minification

Every page is minified. Laravel does not do this by default, and there does not seem to be a "standard" way to do it, other than downloading some shady package.

I've implemented my own simple HTML minifier, making use of PHP's output buffering.

Additionally, CSS & JS files are minified by PurgeCSS and Vite, respectively.

Dependencies

To deploy this website, you need the following:

  • php
  • composer
  • nodejs with npm

Setup

  • Clone the repo: git clone https://github.com/saloniamatteo/openalias
  • Change directory: cd openalias
  • Install PHP dependencies: composer install
  • Install node dependencies: npm i
  • Generate APP_KEY: php artisan key:generate

Note that you also may need to change file permissions and/or owner depending on your setup. If you do, run the following command:

git config core.fileMode false

This stops git from tracking file permission changes.

If you want to deploy the website locally, copy .env.example to .env:

cp .env.example .env

Make sure you modify .env, and uncomment the following:

# Uncomment these values if running locally
APP_ENV=local
APP_DEBUG=true
APP_URL="http://localhost"

The website can now be deployed using the built-in webserver, php artisan serve: it will be reachable at localhost on port 8000.

If you want to use the built-in webserver, make sure you set APP_URL to your website's URL.

If you want to serve this website to the Internet, please make sure you don't use php spark serve, and rather have a real server. I use nginx with FastCGI.

Make sure you also disable access to /build/assets/manifest.json!

When updating, you may use the update.sh script under the scripts/ folder:

./scripts/update.sh

This does the following:

  • Installs the composer + npm dependencies
  • Generates a new key, forcefully
  • Bundles the assets
  • Caches config, events, routes, views.

Assets

Make sure you bundle the assets used in the website (CSS, fonts, images):

npm run build

Cache

When running in production, it is recommended to cache PHP assets with the following command:

composer cache

This will cache PHP config, events, routes, views.

Sample nginx config

Note: this config makes the following assumptions:

  • Your site is hosted at oa.example.com
  • You use LetsEncrypt (certbot) and have deployed an SSL certificate
  • Your nginx build supports HTTP2 and HTTP3 (QUIC)
  • You have IPv6 support enabled
  • You use port 80 for HTTP and port 443 for HTTPS
  • You use php-fpm (FastCGI) and call it via /var/run/php-fpm.sock
  • You want to disable client uploads
  • You want to redirect every HTTP request to the HTTPS port
  • You want to allow robots.txt
  • You want to disable .well-known

Make sure you movify everything that says "Change this"!

# Optional: Rate-limits
#limit_req_zone $binary_remote_addr zone=oa_css:10m rate=10r/s;
#limit_req_zone $binary_remote_addr zone=oa_img:10m rate=5r/s;

# Optional: Bandwidth limits
#limit_conn_zone $binary_remote_addr zone=oa_conn_css:10m;
#limit_conn_zone $binary_remote_addr zone=oa_conn_img:10m;

server {
	# HTTP/1.1 & HTTP/2
	listen 443 ssl;
	listen [::]:443 ssl;

	# HTTP/3 (QUIC)
	listen 443 quic;
	listen [::]:443 quic;

	# Change this!
	server_name oa.example.com;

	# If the host isn't oa.example.com, redirect the client
	if ($host != oa.example.com) {
		return 301 https://oa.example.com$request_uri;
	}

	# HTTP2/3
	http2 on;
	http3 on;
	quic_gso on;
	quic_retry on;
	ssl_early_data on;

	# SSL
	ssl_stapling on;
	ssl_stapling_verify on;
	include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_certificate /etc/letsencrypt/live/oa.example.com/fullchain.pem; # Change this!
    ssl_certificate_key /etc/letsencrypt/live/oa.example.com/privkey.pem; # Change this!
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

	# Site root
	# Change this!
	root /var/www/oa/public;

	# Prevent nginx HTTP Server Detection
	server_tokens off;

	# Only allow GET requests
	if ($request_method !~* ^GET$) {
		return 405;
	}

	# Disable uploads
	client_max_body_size 0;
	client_body_timeout 0s;
	fastcgi_buffers 64 4K;

	# The settings allows you to optimize the HTTP2 bandwidth.
	# See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements for tuning hints
	client_body_buffer_size 512k;

	# Specify how to handle directories -- specifying `/index.php$request_uri`
	# here as the fallback means that Nginx always exhibits the desired behaviour
	# when a client requests a path that corresponds to a directory that exists
	# on the server. In particular, if that directory contains an index.php file,
	# that file is correctly served; if it doesn't, then the request is passed to
	# the front-end controller. This consistent behaviour means that we don't need
	# to specify custom rules for certain paths (e.g. images and other assets,
	# `/updater`, `/ocm-provider`, `/ocs-provider`), and thus
	# `try_files $uri $uri/ /index.php$request_uri`
	# always provides the desired behaviour.
	index index.php index.html /index.php$request_uri;

	# Allow robots.txt
	location = /robots.txt {
		allow all;
		log_not_found off;
	}

	# Allow .well-known/security.txt
	location = /.well-known/security.txt {
		allow all;
		log_not_found off;
	}

	# Prepend all requests with "/index.php" -- this acts as our front controller.
	# index.php handles all requests, but we have to hide it.
	# The line below allows us to do exactly what we want.
	location / {
		rewrite ^ /index.php;
    }

	# Ensure this block, which passes PHP files to the PHP process, is above the blocks
	# which handle static assets (as seen below). If this block is not declared first,
	# then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
	# to the URI, resulting in a HTTP 500 error response.
	location ~ \.php(?:$|/) {
		fastcgi_split_path_info ^(.+?\.php)(/.*)$;
		set $path_info $fastcgi_path_info;

		# Try to load requested file. If it doesn't exist, instead
		# of throwing a 404, load the front controller, where
		# we can load a pretty 404 page.
		try_files $fastcgi_script_name /index.php/$fastcgi_script_name;

		include fastcgi_params;
		fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
		fastcgi_param PATH_INFO $path_info;
		fastcgi_param HTTPS on;

		fastcgi_param modHeadersAvailable true;		 # Avoid sending the security headers twice
		fastcgi_param front_controller_active true;	 # Enable pretty urls
		fastcgi_pass unix:/var/run/php-fpm.sock;

		fastcgi_intercept_errors on;
		fastcgi_request_buffering off;
		fastcgi_max_temp_file_size 0;

		# Remove X-Powered-By, which is an information leak
		fastcgi_hide_header X-Powered-By;

		# Do not show ratelimit
		fastcgi_hide_header X-Ratelimit-Limit;
		fastcgi_hide_header X-Ratelimit-Remaining;

		# Enable gzip but do not remove ETag headers
		gzip on;
		gzip_vary on;
		gzip_comp_level 4;
		gzip_min_length 256;
		gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
		gzip_types text/plain;

		# Inform clients that HTTP3 is available
		add_header Alt-Svc 'h3=":443"; ma=86400';

		# COOP/COEP. Disable if you use external plugins/images/assets
		add_header Cross-Origin-Opener-Policy "same-origin" always;
		add_header Cross-Origin-Embedder-Policy "require-corp" always;
		add_header Cross-Origin-Resource-Policy "same-origin" always;

		# Content Security Policy
		# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
		# This policy allows only internal assets.
		add_header Content-Security-Policy "default-src 'self'; img-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; child-src 'self';";

		# HSTS
		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

		# HTTP response headers borrowed from Nextcloud `.htaccess`
		add_header Referrer-Policy "no-referrer";
		add_header X-Content-Type-Options "nosniff";
		add_header X-Download-Options "noopen";
		add_header X-Frame-Options "SAMEORIGIN";
		add_header X-Permitted-Cross-Domain-Policies "none";
		add_header X-XSS-Protection "0";

		# Tell browsers to use per-origin process isolation
		add_header Origin-Agent-Cluster "?1" always;
	}

	# CSS & JS
	location ~ \.(?:css|js|woff2)$ {
		# Limit access to CSS & JS
		# Set a burst of 15, and start delaying after the 10th req.
		#limit_req zone=oa_css burst=15 delay=10;
		#limit_req_log_level warn;
		#limit_req_status 429;

		# Cap bandwidth to 1MB/s after 1MB,
		# allowing 5 concurrent requests
		#limit_conn oa_conn_css 5;
		#limit_rate_after 1M;
		#limit_rate 1M;

		# Enable gzip but do not remove ETag headers
		gzip on;
		gzip_vary on;
		gzip_comp_level 4;
		gzip_min_length 256;
		gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
		gzip_types font/woff2 text/css text/javascript text/plain;

		add_header Alt-Svc 'h3=":443"; ma=86400';
		add_header X-Content-Type-Options "nosniff";
		add_header X-Frame-Options "SAMEORIGIN";
		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

		try_files $uri /index.php$request_uri;

		expires 10d;
		access_log off;
	}

	# Images
	location ~ \.(?:gif|ico|jpg|jpeg|pdf|png|svg|webp)$ {
		# Limit access to images
		# Set a burst of 10, and start delaying after the 5th req.
		#limit_req zone=oa_img burst=10 delay=5;
		#limit_req_log_level warn;
		#limit_req_status 429;

		# Cap bandwidth to 1MB/s after 1MB,
		# allowing 5 concurrent requests
		#limit_conn oa_conn_img 5;
		#limit_rate_after 1M;
		#limit_rate 1M;

		add_header Alt-Svc 'h3=":443"; ma=86400';
		add_header X-Content-Type-Options "nosniff";
		add_header X-Frame-Options "SAMEORIGIN";
		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

		try_files $uri /index.php$request_uri;

		expires 14d;
		access_log off;
	}
}

server {
	listen 80;
	listen [::]:80;

	# Change this!
	server_name oa.example.com;

	# Prevent nginx HTTP Server Detection
	server_tokens off;

	# Change this!
	return 301 https://oa.example.com$request_uri;
}