Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/Fetcher/SecurityTxtFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Spaze\SecurityTxt\SecurityTxt;
use Spaze\SecurityTxt\Violations\SecurityTxtContentTypeInvalid;
use Spaze\SecurityTxt\Violations\SecurityTxtContentTypeWrongCharset;
use Spaze\SecurityTxt\Violations\SecurityTxtSchemeNotHttps;
use Spaze\SecurityTxt\Violations\SecurityTxtTopLevelDiffers;
use Spaze\SecurityTxt\Violations\SecurityTxtTopLevelPathOnly;
use Spaze\SecurityTxt\Violations\SecurityTxtWellKnownPathOnly;
Expand Down Expand Up @@ -219,10 +218,6 @@ private function getResult(SecurityTxtFetcherFetchHostResult $wellKnown, Securit
} elseif ($contentTypeHeader->getLowercaseCharset() !== SecurityTxt::CHARSET) {
$errors[] = new SecurityTxtContentTypeWrongCharset($result->getUrl(), $contentTypeHeader->getContentType(), $contentTypeHeader->getCharset());
}
$scheme = parse_url($result->getUrl(), PHP_URL_SCHEME);
if ($scheme !== 'https') {
$errors[] = new SecurityTxtSchemeNotHttps($result->getUrl());
}
return new SecurityTxtFetchResult(
$result->getUrl(),
$result->getFinalUrl(),
Expand Down
6 changes: 6 additions & 0 deletions src/Json/SecurityTxtJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ public function createSecurityTxtFromJsonValues(array $values): SecurityTxt
{
$securityTxt = new SecurityTxt(SecurityTxtValidationLevel::AllowInvalidValuesSilently);
try {
if (isset($values['fileLocation'])) {
if (!is_string($values['fileLocation'])) {
throw new SecurityTxtCannotParseJsonException('fileLocation is not a string');
}
$securityTxt->setFileLocation($values['fileLocation']);
}
if (isset($values['expires'])) {
if (!is_array($values['expires'])) {
throw new SecurityTxtCannotParseJsonException('expires is not an array');
Expand Down
7 changes: 5 additions & 2 deletions src/Parser/SecurityTxtParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private function processField(int $lineNumber, string $value, SecurityTxtField $
/**
* @throws SecurityTxtCannotVerifySignatureException
*/
public function parseString(string $contents, ?int $expiresWarningThreshold = null, bool $strictMode = false): SecurityTxtParseStringResult
public function parseString(string $contents, ?string $fileLocation = null, ?int $expiresWarningThreshold = null, bool $strictMode = false): SecurityTxtParseStringResult
{
$this->expiresWarningThreshold = $expiresWarningThreshold;
$this->initFieldProcessors();
Expand All @@ -125,6 +125,9 @@ public function parseString(string $contents, ?int $expiresWarningThreshold = nu
SecurityTxtField::cases(),
);
$securityTxt = new SecurityTxt(SecurityTxtValidationLevel::AllowInvalidValues);
if ($fileLocation !== null) {
$securityTxt->setFileLocation($fileLocation);
}
for ($lineNumber = 1; $lineNumber <= count($lines); $lineNumber++) {
$line = trim($lines[$lineNumber - 1]);
if (!str_ends_with($lines[$lineNumber - 1], "\n")) {
Expand Down Expand Up @@ -170,7 +173,7 @@ public function parseString(string $contents, ?int $expiresWarningThreshold = nu
*/
public function parseFetchResult(SecurityTxtFetchResult $fetchResult, ?int $expiresWarningThreshold = null, bool $strictMode = false): SecurityTxtParseHostResult
{
$parseResult = $this->parseString($fetchResult->getContents(), $expiresWarningThreshold, $strictMode);
$parseResult = $this->parseString($fetchResult->getContents(), $fetchResult->getFinalUrl(), $expiresWarningThreshold, $strictMode);
return new SecurityTxtParseHostResult(
$parseResult->isValid() && $fetchResult->getErrors() === [] && (!$strictMode || $fetchResult->getWarnings() === []),
$parseResult,
Expand Down
71 changes: 55 additions & 16 deletions src/SecurityTxt.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use Spaze\SecurityTxt\Violations\SecurityTxtEncryptionNotUri;
use Spaze\SecurityTxt\Violations\SecurityTxtExpired;
use Spaze\SecurityTxt\Violations\SecurityTxtExpiresTooLong;
use Spaze\SecurityTxt\Violations\SecurityTxtFileLocationNotHttps;
use Spaze\SecurityTxt\Violations\SecurityTxtFileLocationNotUri;
use Spaze\SecurityTxt\Violations\SecurityTxtHiringNotHttps;
use Spaze\SecurityTxt\Violations\SecurityTxtHiringNotUri;
use Spaze\SecurityTxt\Violations\SecurityTxtPolicyNotHttps;
Expand All @@ -42,6 +44,7 @@ final class SecurityTxt implements JsonSerializable
public const string CHARSET = 'charset=utf-8';
public const string CONTENT_TYPE_HEADER = self::CONTENT_TYPE . '; ' . self::CHARSET;

private ?string $fileLocation = null;
private ?SecurityTxtExpires $expires = null;
private ?SecurityTxtSignatureVerifyResult $signatureVerifyResult = null;
private ?SecurityTxtPreferredLanguages $preferredLanguages = null;
Expand Down Expand Up @@ -88,13 +91,32 @@ public function __construct(
}


public function setFileLocation(string $fileLocation): void
{
$this->setValue(
function () use ($fileLocation): void {
$this->fileLocation = $fileLocation;
},
function () use ($fileLocation): void {
$this->checkUri($fileLocation, SecurityTxtFileLocationNotUri::class, SecurityTxtFileLocationNotHttps::class);
},
);
}


public function getFileLocation(): ?string
{
return $this->fileLocation;
}


/**
* @throws SecurityTxtError
* @throws SecurityTxtWarning
*/
public function setExpires(SecurityTxtExpires $expires): void
{
$this->setValue(
$this->setFieldValue(
function () use ($expires): SecurityTxtExpires {
return $this->expires = $expires;
},
Expand Down Expand Up @@ -137,7 +159,7 @@ public function getSignatureVerifyResult(): ?SecurityTxtSignatureVerifyResult
*/
public function addCanonical(SecurityTxtCanonical $canonical): void
{
$this->setValue(
$this->setFieldValue(
function () use ($canonical): SecurityTxtCanonical {
return $this->canonical[] = $canonical;
},
Expand All @@ -162,7 +184,7 @@ public function getCanonical(): array
*/
public function addContact(SecurityTxtContact $contact): void
{
$this->setValue(
$this->setFieldValue(
function () use ($contact): SecurityTxtContact {
return $this->contact[] = $contact;
},
Expand All @@ -187,7 +209,7 @@ public function getContact(): array
*/
public function setPreferredLanguages(SecurityTxtPreferredLanguages $preferredLanguages): void
{
$this->setValue(
$this->setFieldValue(
function () use ($preferredLanguages): SecurityTxtPreferredLanguages {
return $this->preferredLanguages = $preferredLanguages;
},
Expand Down Expand Up @@ -231,7 +253,7 @@ public function getPreferredLanguages(): ?SecurityTxtPreferredLanguages
*/
public function addAcknowledgments(SecurityTxtAcknowledgments $acknowledgments): void
{
$this->setValue(
$this->setFieldValue(
function () use ($acknowledgments): SecurityTxtAcknowledgments {
return $this->acknowledgments[] = $acknowledgments;
},
Expand All @@ -256,7 +278,7 @@ public function getAcknowledgments(): array
*/
public function addHiring(SecurityTxtHiring $hiring): void
{
$this->setValue(
$this->setFieldValue(
function () use ($hiring): SecurityTxtHiring {
return $this->hiring[] = $hiring;
},
Expand All @@ -281,7 +303,7 @@ public function getHiring(): array
*/
public function addPolicy(SecurityTxtPolicy $policy): void
{
$this->setValue(
$this->setFieldValue(
function () use ($policy): SecurityTxtPolicy {
return $this->policy[] = $policy;
},
Expand All @@ -306,7 +328,7 @@ public function getPolicy(): array
*/
public function addEncryption(SecurityTxtEncryption $encryption): void
{
$this->setValue(
$this->setFieldValue(
function () use ($encryption): SecurityTxtEncryption {
return $this->encryption[] = $encryption;
},
Expand All @@ -327,33 +349,49 @@ public function getEncryption(): array


/**
* @param callable(): SecurityTxtFieldValue $setValue
* @param callable(): void $setValue
* @param callable(): void $validator
* @param (callable(): void)|null $warnings
* @return void
*/
private function setValue(callable $setValue, callable $validator, ?callable $warnings = null): void
private function setValue(callable $setValue, callable $validator): void
{
if ($this->validationLevel === SecurityTxtValidationLevel::AllowInvalidValuesSilently) {
$this->orderedFields[] = $setValue();
$setValue();
return;
}
if ($this->validationLevel === SecurityTxtValidationLevel::AllowInvalidValues) {
$this->orderedFields[] = $setValue();
$setValue();
$validator();
} else {
$validator();
$this->orderedFields[] = $setValue();
$setValue();
}
}


/**
* @param callable(): SecurityTxtFieldValue $setValue
* @param callable(): void $validator
* @param (callable(): void)|null $warnings
* @return void
*/
private function setFieldValue(callable $setValue, callable $validator, ?callable $warnings = null): void
{
$this->setValue(
function () use ($setValue): void {
$this->orderedFields[] = $setValue();
},
$validator,
);
if ($warnings !== null) {
$warnings();
}
}


/**
* @param class-string<SecurityTxtAcknowledgmentsNotUri|SecurityTxtCanonicalNotUri|SecurityTxtContactNotUri|SecurityTxtEncryptionNotUri|SecurityTxtHiringNotUri|SecurityTxtPolicyNotUri> $notUriError
* @param class-string<SecurityTxtAcknowledgmentsNotHttps|SecurityTxtCanonicalNotHttps|SecurityTxtContactNotHttps|SecurityTxtEncryptionNotHttps|SecurityTxtHiringNotHttps|SecurityTxtPolicyNotHttps> $notHttpsError
* @param class-string<SecurityTxtAcknowledgmentsNotUri|SecurityTxtCanonicalNotUri|SecurityTxtContactNotUri|SecurityTxtEncryptionNotUri|SecurityTxtHiringNotUri|SecurityTxtPolicyNotUri|SecurityTxtFileLocationNotUri> $notUriError
* @param class-string<SecurityTxtAcknowledgmentsNotHttps|SecurityTxtCanonicalNotHttps|SecurityTxtContactNotHttps|SecurityTxtEncryptionNotHttps|SecurityTxtHiringNotHttps|SecurityTxtPolicyNotHttps|SecurityTxtFileLocationNotHttps> $notHttpsError
* @throws SecurityTxtError
*/
private function checkUri(string $uri, string $notUriError, string $notHttpsError): void
Expand Down Expand Up @@ -384,6 +422,7 @@ public function getOrderedFields(): array
public function jsonSerialize(): array
{
return [
'fileLocation' => $this->getFileLocation(),
'expires' => $this->getExpires(),
'signatureVerifyResult' => $this->getSignatureVerifyResult(),
'preferredLanguages' => $this->getPreferredLanguages(),
Expand Down
2 changes: 2 additions & 0 deletions src/Validator/SecurityTxtValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Spaze\SecurityTxt\Exceptions\SecurityTxtError;
use Spaze\SecurityTxt\Exceptions\SecurityTxtWarning;
use Spaze\SecurityTxt\SecurityTxt;
use Spaze\SecurityTxt\Validator\Validators\CanonicalUriListedFieldValidator;
use Spaze\SecurityTxt\Validator\Validators\ContactMissingFieldValidator;
use Spaze\SecurityTxt\Validator\Validators\ExpiresMissingFieldValidator;
use Spaze\SecurityTxt\Validator\Validators\FieldValidator;
Expand All @@ -23,6 +24,7 @@ final class SecurityTxtValidator
public function __construct()
{
$this->fieldValidators = [
new CanonicalUriListedFieldValidator(),
new ContactMissingFieldValidator(),
new ExpiresMissingFieldValidator(),
new SignedButCanonicalMissingFieldValidator(),
Expand Down
33 changes: 33 additions & 0 deletions src/Validator/Validators/CanonicalUriListedFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);

namespace Spaze\SecurityTxt\Validator\Validators;

use Override;
use Spaze\SecurityTxt\Exceptions\SecurityTxtWarning;
use Spaze\SecurityTxt\SecurityTxt;
use Spaze\SecurityTxt\Violations\SecurityTxtCanonicalUriMismatch;

final class CanonicalUriListedFieldValidator implements FieldValidator
{

#[Override]
public function validate(SecurityTxt $securityTxt): void
{
$uri = $securityTxt->getFileLocation();
if ($uri === null) {
return;
}

$canonicals = $securityTxt->getCanonical();
if ($canonicals === []) {
return;
}

$canonicalUris = array_map(fn($canonical) => $canonical->getUri(), $canonicals);
if (!in_array($uri, $canonicalUris, true)) {
throw new SecurityTxtWarning(new SecurityTxtCanonicalUriMismatch($uri, $canonicalUris));
}
}

}
37 changes: 37 additions & 0 deletions src/Violations/SecurityTxtCanonicalUriMismatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);

namespace Spaze\SecurityTxt\Violations;

use Spaze\SecurityTxt\Fields\SecurityTxtField;

final class SecurityTxtCanonicalUriMismatch extends SecurityTxtSpecViolation
{

/**
* @param list<string> $canonicalUris
*/
public function __construct(string $uri, array $canonicalUris)
{
$count = count($canonicalUris);
if ($count === 1) {
$messageFormat = 'The file was fetched from %s but the %s field (%s) does not list this URI';
$howToFixFormat = 'Add a new %s field with the URI %s, or ensure the file is fetched from the listed canonical URI';
} else {
$fields = implode(', ', array_fill(0, $count, '%s'));
$messageFormat = 'The file was fetched from %s but none of the %s fields (' . $fields . ') list this URI';
$howToFixFormat = 'Add a new %s field with the URI %s, or ensure the file is fetched from one of the listed canonical URIs';
}
parent::__construct(
func_get_args(),
$messageFormat,
[$uri, SecurityTxtField::Canonical->value, ...$canonicalUris],
'draft-foudil-securitytxt-05',
null,
$howToFixFormat,
[SecurityTxtField::Canonical->value, $uri],
'2.5.2',
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@

namespace Spaze\SecurityTxt\Violations;

final class SecurityTxtSchemeNotHttps extends SecurityTxtSpecViolation
final class SecurityTxtFileLocationNotHttps extends SecurityTxtSpecViolation
{

public function __construct(string $url)
public function __construct(string $uri)
{
parent::__construct(
func_get_args(),
"The file at %s must use HTTPS",
[$url],
[$uri],
'draft-foudil-securitytxt-06',
preg_replace('~^http://~', 'https://', $url),
preg_replace('~^http://~', 'https://', $uri),
'Use HTTPS to serve the %s file',
['security.txt'],
'3',
Expand Down
23 changes: 23 additions & 0 deletions src/Violations/SecurityTxtFileLocationNotUri.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);

namespace Spaze\SecurityTxt\Violations;

final class SecurityTxtFileLocationNotUri extends SecurityTxtSpecViolation
{

public function __construct(string $uri)
{
parent::__construct(
func_get_args(),
"The location of the file %s doesn't follow the URI syntax described in RFC 3986, the scheme is missing",
[$uri],
'draft-foudil-securitytxt-00',
null,
'Use a URI as the value',
[],
'3',
);
}

}
2 changes: 1 addition & 1 deletion tests/Check/SecurityTxtCheckHostResultFactoryTest.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ final class SecurityTxtCheckHostResultFactoryTest extends TestCase
"Expires: 2020-10-15T00:01:02+02:00\n",
];
$contents = implode('', $lines);
$parseStringResult = $this->parser->parseString($contents, 123, true);
$parseStringResult = $this->parser->parseString($contents, null, 123, true);
$fetchResult = new SecurityTxtFetchResult(
'https://com.example/.well-known/security.txt',
'https://com.example/.well-known/security.txt',
Expand Down
Loading