-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from istiak-tridip/develop
Merge develop branch
- Loading branch information
Showing
12 changed files
with
373 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.idea | ||
vendor | ||
.php_cs.cache | ||
composer.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
$finder = Symfony\Component\Finder\Finder::create() | ||
->in([ | ||
__DIR__ . '/app', | ||
]) | ||
->name('*.php') | ||
->ignoreDotFiles(true) | ||
->ignoreVCS(true); | ||
|
||
return PhpCsFixer\Config::create() | ||
->setRules([ | ||
'@PSR2' => true, | ||
'array_syntax' => ['syntax' => 'short'], | ||
'ordered_imports' => ['sortAlgorithm' => 'alpha'], | ||
'no_unused_imports' => true, | ||
'trailing_comma_in_multiline_array' => true, | ||
'phpdoc_scalar' => true, | ||
'unary_operator_spaces' => true, | ||
'blank_line_before_statement' => [ | ||
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], | ||
], | ||
'phpdoc_single_line_var_spacing' => true, | ||
'phpdoc_var_without_name' => true, | ||
'class_attributes_separation' => [ | ||
'elements' => [ | ||
'method', | ||
], | ||
], | ||
'method_argument_space' => [ | ||
'on_multiline' => 'ensure_fully_multiline', | ||
'keep_multiple_spaces_after_comma' => true, | ||
], | ||
'single_trait_insert_per_statement' => true, | ||
]) | ||
->setFinder($finder); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,7 @@ | ||
### About | ||
## About | ||
|
||
This project is a simple implementation of "Origin Pull" CDN with cache support written in PHP 7.4. | ||
|
||
#### Origin Pull CDN | ||
|
||
--- | ||
Origin pull CDN is a type of CDN where you don't have to upload files to the CDN server instead CDN does it for you. You only rewrite URLs to point to the CDN. When asked for a specific file, the CDN will first go to the | ||
original server, pull the file, cache and serve it. | ||
original server, pull the file, cache, and serve it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<?php | ||
|
||
namespace App; | ||
|
||
use App\Responses\ErrorResponse; | ||
use App\Responses\NotAllowedResponse; | ||
use App\Responses\SuccessResponse; | ||
use Doctrine\Common\Cache\FilesystemCache; | ||
use Exception; | ||
use GuzzleHttp\Client; | ||
use Throwable; | ||
|
||
class Response | ||
{ | ||
protected array $config; | ||
|
||
protected FilesystemCache $cache; | ||
|
||
public function __construct(array $config) | ||
{ | ||
$this->config = $config; | ||
} | ||
|
||
public function send(string $path): void | ||
{ | ||
$this->checkIfPathIsAllowed($path); | ||
|
||
try { | ||
$key = $this->getCacheKey($path); | ||
if (!$this->cache()->contains($key)) { | ||
$this->fetchFileContent($path); | ||
} | ||
|
||
$content = $this->cache()->fetch($path); | ||
$response = new SuccessResponse($content); | ||
$response->send(); | ||
} catch (Throwable $exception) { | ||
$response = new ErrorResponse($exception); | ||
$response->send(); | ||
} | ||
} | ||
|
||
protected function fetchFileContent(string $path): void | ||
{ | ||
$client = $this->client(); | ||
$response = $client->get($path); | ||
|
||
$key = $this->getCacheKey($path); | ||
if (!$this->cacheFileContent($key, $response->getBody())) { | ||
throw new Exception("Failed to cache file content."); | ||
} | ||
} | ||
|
||
protected function cacheFileContent(string $path, string $contents): bool | ||
{ | ||
return $this->cache()->save( | ||
$path, | ||
$contents, | ||
$this->config["cache"]["lifetime"] | ||
); | ||
} | ||
|
||
protected function client(): Client | ||
{ | ||
return new Client([ | ||
"base_uri" => $this->config["origin"]["base_uri"], | ||
"timeout" => $this->config["origin"]["timeout"], | ||
"headers" => [ | ||
"User-Agent" => "OPCDN-BOT/0.1.0", | ||
], | ||
]); | ||
} | ||
|
||
protected function cache(): FilesystemCache | ||
{ | ||
return $this->cache ??= | ||
new FilesystemCache($this->config["cache"]["directory"], ".it"); | ||
} | ||
|
||
protected function checkIfPathIsAllowed(string $path): void | ||
{ | ||
if (empty($paths = $this->config["origin"]["paths"])) { | ||
return; | ||
} | ||
|
||
foreach ($paths as $pattern) { | ||
if ($this->pathMatches($pattern, $path) === false) { | ||
$response = new NotAllowedResponse($path); | ||
$response->send(); | ||
} | ||
} | ||
} | ||
|
||
protected function pathMatches(string $pattern, string $path): bool | ||
{ | ||
if ($pattern === $path) { | ||
return true; | ||
} | ||
|
||
$path = trim($path, "/"); | ||
|
||
return preg_match('#^' . $pattern . '\z#u', $path) === 1; | ||
} | ||
|
||
protected function getCacheKey(string $path): string | ||
{ | ||
$host = parse_url($this->config["origin"]["base_uri"], PHP_URL_HOST); | ||
|
||
return $host . "::" . $path; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?php | ||
|
||
namespace App\Responses; | ||
|
||
use GuzzleHttp\Exception\RequestException; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Throwable; | ||
|
||
class ErrorResponse implements ResponseInterface | ||
{ | ||
private Throwable $exception; | ||
|
||
public function __construct(Throwable $exception) | ||
{ | ||
$this->exception = $exception; | ||
} | ||
|
||
public function send(): void | ||
{ | ||
$response = new Response(); | ||
$response->setContent($this->getContent()); | ||
$response->setStatusCode($this->getStatusCode()); | ||
|
||
$response->send(); | ||
} | ||
|
||
public function getContent(): string | ||
{ | ||
return Response::$statusTexts[$this->getStatusCode()] ?? "Unknown Error"; | ||
} | ||
|
||
public function getStatusCode(): int | ||
{ | ||
if ($this->exception instanceof RequestException && $this->exception->hasResponse()) { | ||
$statusCode = $this->exception->getResponse()->getStatusCode(); | ||
} | ||
|
||
return $statusCode ?? Response::HTTP_FAILED_DEPENDENCY; | ||
} | ||
|
||
public function getContentType(): string | ||
{ | ||
return ""; | ||
} | ||
|
||
public function getContentLength(): int | ||
{ | ||
return 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
namespace App\Responses; | ||
|
||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
class NotAllowedResponse implements ResponseInterface | ||
{ | ||
private string $path; | ||
|
||
public function __construct(string $path) | ||
{ | ||
$this->path = $path; | ||
} | ||
|
||
public function send(): void | ||
{ | ||
$response = new Response(); | ||
$response->setContent($this->getContent()); | ||
$response->setStatusCode($this->getStatusCode()); | ||
|
||
$response->send(); | ||
exit; | ||
} | ||
|
||
public function getContent(): string | ||
{ | ||
return "Path ({$this->path}) is not allowed."; | ||
} | ||
|
||
public function getStatusCode(): int | ||
{ | ||
return Response::HTTP_BAD_REQUEST; | ||
} | ||
|
||
public function getContentType(): string | ||
{ | ||
return ""; | ||
} | ||
|
||
public function getContentLength(): int | ||
{ | ||
return 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace App\Responses; | ||
|
||
interface ResponseInterface | ||
{ | ||
public function send(): void; | ||
|
||
public function getContent(): string; | ||
|
||
public function getStatusCode(): int; | ||
|
||
public function getContentType(): string; | ||
|
||
public function getContentLength(): int; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<?php | ||
|
||
namespace App\Responses; | ||
|
||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse; | ||
|
||
class SuccessResponse implements ResponseInterface | ||
{ | ||
private string $content; | ||
|
||
public function __construct(string $content) | ||
{ | ||
$this->content = $content; | ||
} | ||
|
||
public function send(): void | ||
{ | ||
$response = new SymfonyResponse(); | ||
$response->setContent($this->getContent()); | ||
$response->setStatusCode($this->getStatusCode()); | ||
$response->headers->set("Content-Type", $this->getContentType()); | ||
$response->headers->set("Content-Length", $this->getContentLength()); | ||
|
||
$response->send(); | ||
} | ||
|
||
public function getContent(): string | ||
{ | ||
return $this->content; | ||
} | ||
|
||
public function getStatusCode(): int | ||
{ | ||
return Response::HTTP_OK; | ||
} | ||
|
||
public function getContentType(): string | ||
{ | ||
return finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $this->getContent()); | ||
} | ||
|
||
public function getContentLength(): int | ||
{ | ||
return strlen($this->getContent()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "istiak/origin-pull-cdn", | ||
"description": "A simple implementation of \"Origin Pull\" CDN with cache support.", | ||
"type": "project", | ||
"require": { | ||
"php": "^7.4|^8.0", | ||
"ext-fileinfo": "*", | ||
"doctrine/cache": "^1.10", | ||
"guzzlehttp/guzzle": "^7.2", | ||
"symfony/http-foundation": "^5.2" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"App\\": "app/" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?php | ||
|
||
return [ | ||
"origin" => [ | ||
// Base URI is used with relative requests | ||
"base_uri" => "https://istiaktridip.net", | ||
|
||
// Float describing the total timeout of the request in seconds. | ||
// Use 0 to wait indefinitely. | ||
"timeout" => 5.0, | ||
|
||
// The origin URIs that should be served from this CDN. | ||
// Leave the array empty if you want to server all origin URIs. | ||
"paths" => [ | ||
"images/.*", | ||
], | ||
], | ||
"cache" => [ | ||
// Absolute Path of the directory where cache entries will be stored. | ||
"directory" => __DIR__ . "/storage", | ||
|
||
// The lifetime in number of seconds for a cache entry. | ||
// Use 0 to store the entry indefinitely. | ||
"lifetime" => 0, | ||
], | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
require_once __DIR__ . "/../vendor/autoload.php"; | ||
|
||
error_reporting(E_ALL); | ||
ini_set("display_errors", "On"); | ||
|
||
/** | ||
* Get the config & requested path | ||
*/ | ||
$config = require __DIR__ . "/../config.php"; | ||
$urlPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); | ||
|
||
/** | ||
* Run The Application | ||
*/ | ||
(new \App\Response($config))->send($urlPath); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* | ||
!.gitignore |