Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add privacy protection along with some CI tests #3

Merged
merged 13 commits into from
Nov 18, 2024
37 changes: 37 additions & 0 deletions .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: lint-test

on:
push:
branches:
- '**'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint-test:
name: lint+test
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ["8.1", "8.2", "8.3"]
drupal-version: ["10.2", "10.3", "10.4", "11.0"]
exclude:
- drupal-version: "11.0"
php-version: "8.1"
- drupal-version: "11.0"
php-version: "8.2"
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: lint+test
working-directory: tests
run: |
export MODULE_DIRECTORY=$(pwd | xargs dirname)
docker compose up --quiet-pull --abort-on-container-exit
env:
DRUPAL_VERSION: ${{ matrix.drupal-version }}
PHP_VERSION: ${{ matrix.php-version }}
ENABLE_MODULES: turnstile_protect
36 changes: 0 additions & 36 deletions .github/workflows/lint.yml

This file was deleted.

28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Create release
on:
pull_request_target:
branches:
- main
types:
- closed
permissions:
contents: write
actions: write
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: install autotag binary
run: curl -sL https://git.io/autotag-install | sudo sh -s -- -b /usr/bin

- name: create release
run: |-
TAG=$(autotag)
git tag $TAG
git push origin $TAG
20 changes: 20 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "drupal/turnstile_protect",
"description": "Put your site routes behind a Cloudflare Turnstile",
"type": "drupal-module",
"license": "GPL-2.0+",
"homepage": "https://www.drupal.org/project/turnstile_protect",
"support": {
"issues": "https://www.drupal.org/project/issues/turnstile_protect"
},
"authors": [
{
"name": "Joe Corall",
"email": "[email protected]",
"role": "Owner"
}
],
"require" : {
"drupal/turnstile": "^1"
}
}
2 changes: 2 additions & 0 deletions config/schema/turnstile_protect.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ turnstile_protect.settings:
label: 'Turnstile Protect settings'
mapping:
routes:
type: sequence
sequence:
type: string
label: 'Routes'
bots:
type: sequence
sequence:
type: string
label: 'List of good bot domains'
Expand Down
9 changes: 6 additions & 3 deletions src/EventSubscriber/Challenge.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,20 @@ protected function applies(RequestEvent $event, ImmutableConfig $config): bool {
if (captcha_whitelist_ip_whitelisted($clientIp)) {
return FALSE;
}

// See if the client IP resolves to a good bot.
$hostname = gethostbyaddr($clientIp);
// Being sure to lookup the domain to avoid spoofing.
$resolved_ip = gethostbyname($hostname);
if ($clientIp !== $resolved_ip) {
return TRUE;
if ($clientIp !== '127.0.0.1') {
return TRUE;
}
}
$parts = explode(".", $hostname);
if (count($parts) < 2) {
return TRUE;
if ($clientIp !== '127.0.0.1') {
return TRUE;
}
}
$tld = array_pop($parts);
$hostname = array_pop($parts) . '.' . $tld;
Expand Down
22 changes: 22 additions & 0 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
networks:
default:
services:
chromedriver:
image: drupalci/webdriver-chromedriver:production
entrypoint:
- chromedriver
- "--log-path=/dev/null"
- "--verbose"
- "--allowed-ips="
- "--allowed-origins=*"
drupal:
image: lehighlts/drupal-ci:${DRUPAL_VERSION}-php${PHP_VERSION}
volumes:
- ${MODULE_DIRECTORY}:/var/www/drupal/web/modules/contrib/${ENABLE_MODULES}
environment:
SIMPLETEST_BASE_URL: http://drupal:8282
ENABLE_MODULES: ${ENABLE_MODULES}
MINK_DRIVER_ARGS_WEBDRIVER: '["chrome", {"browserName":"chrome","goog:chromeOptions":{"args":["--disable-gpu","--headless", "--no-sandbox", "--disable-dev-shm-usage"]}}, "http://chromedriver:9515"]'
SYMFONY_DEPRECATIONS_HELPER: weak
links:
- chromedriver
56 changes: 56 additions & 0 deletions tests/src/Functional/NoReferrerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Drupal\Tests\turnstile_protect\Functional;

use Drupal\Tests\BrowserTestBase;

/**
* Tests redirection from /node/1 to /challenge.
*
* @group turnstile_protect
*/
class NoReferrerTest extends BrowserTestBase {

/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'captcha',
'turnstile',
'turnstile_protect',
];

/**
* {@inheritdoc}
*/
protected $defaultTheme = 'claro';

/**
* Sets up the test environment.
*/
protected function setUp(): void {
parent::setUp();

// Always pass the turnstile
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
$this->config('turnstile.settings')
->set('site_key', '1x00000000000000000000AA')
->set('secret_key', '1x0000000000000000000000000000000AA')
->save();
}

/**
* Tests referrerpolicy is set on cloudflare <script>.
*/
public function testReferrer() {
$cloudFlareSrc = \Drupal::config('turnstile.settings')->get('turnstile_src');
$this->drupalGet('/challenge');
$page = $this->getSession()->getPage();
$script = $page->find('css', "script[src='$cloudFlareSrc']");
$this->assertNotNull($script, "Cloudflare script could not be found on page");
$attribute = 'referrerpolicy';
$this->assertTrue($script->hasAttribute($attribute) && $script->getAttribute($attribute) === 'no-referrer', "referrerpolicy not correct");
}

}
85 changes: 85 additions & 0 deletions tests/src/Functional/RedirectTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace Drupal\Tests\turnstile_protect\Functional;

use Drupal\FunctionalJavascriptTests\WebDriverTestBase;

/**
* Tests redirection from /node/1 to /challenge.
*
* @group turnstile_protect
*/
class RedirectTest extends WebDriverTestBase {

/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'captcha',
'turnstile',
'turnstile_protect',
];

/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';

/**
* Sets up the test environment.
*/
protected function setUp(): void {
parent::setUp();

// Always pass the turnstile
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
$this->config('turnstile.settings')
->set('site_key', '1x00000000000000000000AA')
->set('secret_key', '1x0000000000000000000000000000000AA')
->save();

$config = $this->config('turnstile_protect.settings')
->set('routes', ["entity.node.canonical"])
->set('bots', [])
->set('rate_limit', TRUE)
->set('window', 86400)
->set('threshold', 1)
->set('history_enabled', FALSE)
->save();

$this->assertEquals("entity.node.canonical", $config->get('routes')[0], 'Routes configuration is set to node.entity.canonical.');
}

/**
* Tests redirection from node to /challenge.
*/
public function testNodeRedirect() {
// Create a node we'll try accessing.
$node = $this->drupalCreateNode(['type' => 'page']);
$nodeUrl = $node->toUrl()->toString();

// Since turnstile_protect.settings["threshold"] = 1
// we should be able to view the node once.
$this->drupalGet($nodeUrl);
$url = $this->getSession()->getCurrentUrl();
$components = parse_url($url);
$this->assertEquals($nodeUrl, $components['path'], 'User is not redirected to /challenge.');

// We should be challenged now on the second look.
$this->drupalGet($nodeUrl);
$url = $this->getSession()->getCurrentUrl();
$components = parse_url($url);
$this->assertEquals('/challenge', $components['path'], 'User is not redirected to /challenge.');

sleep(15);

// We should be redirected to the node after the turnstile passed
// which it always will in this test with the turnstile site/secret keys
// set to their always pass test.
$url = $this->getSession()->getCurrentUrl();
$components = parse_url($url);
$this->assertEquals($nodeUrl, $components['path'], 'User is redirected back to node.');
}

}
43 changes: 43 additions & 0 deletions tests/src/Kernel/TurnstileProtectTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Drupal\Tests\turnstile_protect\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
* Tests basic functionality of the turnstile_protect module.
*
* @group turnstile_protect
*/
class TurnstileProtectTest extends KernelTestBase {

/**
* {@inheritdoc}
*/
protected static $modules = [
'turnstile_protect',
'system',
];

/**
* Tests if the module is enabled.
*/
public function testModuleEnabled() {
$this->assertTrue(
$this->container->get('module_handler')->moduleExists('turnstile_protect'),
'The turnstile_protect module is enabled.'
);
}

/**
* Tests basic configuration or service.
*/
public function testBasicFunctionality() {
$config = $this->config('turnstile_protect.settings');
$this->assertNotEmpty(
$config,
'turnstile_protect settings configuration is available.'
);
}

}
3 changes: 3 additions & 0 deletions turnstile_protect.module
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ function turnstile_protect_captcha_alter(&$captcha, $info) {
return;
}

// Block referrer to cloudflare.
$captcha['form']['turnstile_widget']['#attached']['html_head'][0][0]['#attributes']['referrerpolicy'] = 'no-referrer';

// Add a javascript callback after the turnstile succeeds.
$captcha['form']['turnstile_widget']['#markup'] = str_replace('<div', '<div data-callback="turnstileProtectAutoSubmit"', $captcha['form']['turnstile_widget']['#markup']);
$captcha['form']['#attached']['library'][] = 'turnstile_protect/challenge';
Expand Down
Loading