├── .github └── workflows │ └── run_tests.yml ├── .gitignore ├── README.md ├── bin └── cactus ├── composer.json ├── src ├── Cactus.php ├── CactusErrorHandler.php └── Exception │ ├── DepedencyException.php │ └── RunTimeException.php └── test └── sampleProject ├── composer.json ├── phpunit.xml ├── tests └── CactusTest.php └── toBeCompiled ├── shouldBeCompiled.php └── shouldNotBeCompiled.txt /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-22.04 13 | 14 | container: 15 | image: php:8.1-cli 16 | options: --user root 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Fix Permissions 22 | run: mkdir /tmp/cache && chmod -R 777 /tmp/cache 23 | 24 | - name: Install Dependencies 25 | run: apt-get update && apt-get install -y git zip unzip 26 | 27 | - name: Install OPCache 28 | run: docker-php-ext-install opcache && docker-php-ext-enable opcache 29 | 30 | - name: Enable OPCache 31 | run: echo 'opcache.enable=1\n opcache.enable_cli=1 \n opcache.validate_timestamps=0\n opcache.file_cache = /tmp/cache\n' >> /usr/local/etc/php/conf.d/opcache.ini 32 | 33 | - name: Install composer 34 | run: curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer 35 | 36 | - name: Validate composer.json and composer.lock 37 | run: composer validate --strict 38 | 39 | - name: Cache Composer packages 40 | id: composer-cache 41 | uses: actions/cache@v2 42 | with: 43 | path: vendor 44 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-php- 47 | 48 | - name: Install dependencies 49 | run: composer install --working-dir=${GITHUB_WORKSPACE}/test/sampleProject --prefer-dist --no-progress 50 | 51 | - name: Compile Files 52 | run: ${GITHUB_WORKSPACE}/test/sampleProject/vendor/bin/cactus --noPrompt --dir toBeCompiled 53 | 54 | - name: Run tests 55 | run: cd ${GITHUB_WORKSPACE}/test/sampleProject && php vendor/phpunit/phpunit/phpunit --configuration phpunit.xml 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | composer.lock 4 | vendor 5 | test/vendor 6 | test/composer.lock 7 | test/.phpunit.result.cache 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Cactus 🌵 2 | 3 | Protect your PHP code from being stolen. Deploy with no fear of not owning servers. This library compiles your PHP code to opcodes and removes code from all php files included in your project. All produced opcode files are saved on the server's filesystem and used by OPcache! 4 | 5 | **Before** 6 | 7 | index.php 8 | 9 | V∆èMi…˝8é/laravel/public/index.phpÈ1QyV∆èMi…˝8é/laravel/public/index.phpVpJįRÄdefineVYZ€∂ 29 | ¨≥î 30 | LARAVEL_STARTVÆó‰H—wÉ microtimeVÑÀTñuä¿ file_existsVΩP∂äÔÓSø4/laravel/public/../storage/framework/maintenance.phpVo~†∑”5∂&/laravel/public/../vendor/autoload.phpV'†fieE^Ô$/laravel/public/../bootstrap/app.phpV£ö|ÄmakeVfl.`ôå¥öê Illuminate\Contracts\Http\KernelVqÿêSÄhandle!V⁄ó≤:·†q”Illuminate\Http\RequestV:¥ˇi?⁄flilluminate\http\requestV9÷ı≤±–ÄcaptureVO€ù|ÄsendVÓ ‘=—ŸwÉ terminateVhÏ1\ 31 | 32 | ## Useful Notes 33 | 34 | - **Your php files content will be replaced!!!!!!!** Be sure **you have 35 | copies** on your development machine 36 | 37 | - If you want to update a file after compilation. Replace the existing 38 | (empty file) with the updated one, and run PHP cactus again 39 | 40 | - The opcodes should be served by the same interpreter which has created them. 41 | - A good idea is to create docker images with your compiled app. Your could update your app by updating your docker image version 42 | 43 | ## Install 44 | 45 | Using Composer 46 | ``` 47 | composer require notihnio/php-cactus:^1.0 48 | ``` 49 | 50 | Add to your php.ini configuration 51 | 52 | opcache.enable=1 53 | opcache.enable_cli=1 54 | opcache.validate_timestamps=0 55 | opcache.file_cache = "Enter here the path where the opcodes will be saved" 56 | 57 | ## Run php Cactus 58 | 59 | YourProjectRootDir/vendor/bin/cactus 60 | 61 | Run php Cactus without any prompt (Force mode for your deployment process) 62 | 63 | YourProjectRootDir/vendor/bin/cactus --noPrompt 64 | 65 | 66 | Run php Cactus for specific project subdirectory (dir parameter should be relative to project root directory) 67 | 68 | YourProjectRootDir/vendor/bin/cactus --dir app/Controllers 69 | -------------------------------------------------------------------------------- /bin/cactus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | getMessage()}\n"; 37 | exit(1); 38 | } 39 | 40 | $compilationDir = $projectDir = dirname(__FILE__, 5); 41 | 42 | if (!is_null($requiredDir)) { 43 | $compilationDir = $projectDir.DIRECTORY_SEPARATOR.$requiredDir; 44 | } 45 | 46 | if (!is_dir($compilationDir)) { 47 | echo "Path is not valid\n"; 48 | exit(1); 49 | } 50 | 51 | try { 52 | $cactus->compile($compilationDir); 53 | } catch (\Notihnio\Cactus\Exception\RunTimeException $exception) { 54 | echo "Runtime error: {$exception->getMessage()}\n"; 55 | exit(1); 56 | } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notihnio/php-cactus", 3 | "bin": [ 4 | "bin/cactus" 5 | ], 6 | "description": "Compile and protect your php code from been stolen. No risk deployments to client's servers", 7 | "type": "library", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Notis Mastrandrikos", 12 | "email": "pmastrandrikos@gmail.com" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Notihnio\\Cactus\\": "src/" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=8.0", 22 | "ext-zend-opcache": "*", 23 | "ext-fileinfo": "*" 24 | }, 25 | "minimum-stability": "stable" 26 | } 27 | -------------------------------------------------------------------------------- /src/Cactus.php: -------------------------------------------------------------------------------- 1 | detectPhpConfiguration(); 16 | } 17 | 18 | /** 19 | * @return string 20 | */ 21 | private function detectOs(): string 22 | { 23 | return (PHP_OS_FAMILY === "Windows") ? "Windows" : "Unix"; 24 | } 25 | 26 | /** 27 | * @throws \Notihnio\Cactus\Exception\DepedencyException 28 | */ 29 | private function detectIfOpcacheIsEnabled() : void 30 | { 31 | if (!is_array(opcache_get_status())) { 32 | throw new DepedencyException("Opcache should be enabled"); 33 | 34 | } 35 | } 36 | 37 | /** 38 | * @return string 39 | * @throws \Notihnio\Cactus\Exception\DepedencyException 40 | */ 41 | private function getOpcacheFileCachePath(): string 42 | { 43 | if (!ini_get("opcache.file_cache")) { 44 | throw new DepedencyException("Opcache file cache should be enabled"); 45 | } 46 | 47 | return ini_get("opcache.file_cache"); 48 | } 49 | 50 | /** 51 | * @return void 52 | * @throws \Notihnio\Cactus\Exception\DepedencyException 53 | */ 54 | private function detectIfFileCacheDirIsWritable(): void 55 | { 56 | if (!is_writable($this->getOpcacheFileCachePath())) { 57 | throw new DepedencyException("Opcache file folder should be writable"); 58 | } 59 | } 60 | 61 | /** 62 | * @return void 63 | * @throws \Notihnio\Cactus\Exception\DepedencyException 64 | */ 65 | private function detectOpcacheValidateTimestamps(): void 66 | { 67 | if (ini_get("opcache.validate_timestamps")) { 68 | throw new DepedencyException("opcache.validate_timestamps should not be enabled"); 69 | } 70 | } 71 | 72 | /** 73 | * @return void 74 | * @throws \Notihnio\Cactus\Exception\DepedencyException 75 | */ 76 | private function detectPhpConfiguration(): void 77 | { 78 | $this->detectIfOpcacheIsEnabled(); 79 | $this->detectIfFileCacheDirIsWritable(); 80 | $this->detectOpcacheValidateTimestamps(); 81 | } 82 | 83 | /** 84 | * @param $rootDirectory 85 | * @param bool $isRootDir 86 | * 87 | * @return void 88 | * @throws \Notihnio\Cactus\Exception\RuntimeException 89 | */ 90 | public function compile($rootDirectory, bool $isRootDir = true): void 91 | { 92 | $dirContents = scandir($rootDirectory); 93 | 94 | $phpMimeTypes = [ 95 | "text/php", 96 | "text/x-php", 97 | "application/php", 98 | "application/x-php", 99 | "application/x-httpd-php", 100 | "application/x-httpd-php-source" 101 | ]; 102 | 103 | foreach ($dirContents as $dirChild) { 104 | if ($dirChild === "." || $dirChild === "..") { 105 | continue; 106 | } 107 | 108 | $filePath = $rootDirectory . DIRECTORY_SEPARATOR . $dirChild; 109 | 110 | if (str_ends_with($dirChild, ".php") && 111 | is_file($filePath) && in_array(mime_content_type($filePath), $phpMimeTypes, true) 112 | ) { 113 | 114 | //if file has been compiled from cactus 115 | if($dirChild !== "Cactus.php" && str_contains(file_get_contents($filePath), "compiled by Cactus")) { 116 | 117 | //do not compile it again 118 | echo "Skipping file ${filePath}, has already been compiled\n"; 119 | continue; 120 | } 121 | 122 | //if file exists in cache 123 | if (opcache_is_script_cached($filePath)) { 124 | 125 | //invalidate cache and let cactus compile the file again 126 | opcache_invalidate($filePath, true); 127 | } 128 | 129 | echo "Compiling ${filePath}\n"; 130 | if (!is_writable($filePath)) { 131 | throw new RuntimeException("No permissions to write file {$filePath}"); 132 | } 133 | 134 | try { 135 | CactusErrorHandler::start(E_WARNING); 136 | 137 | if (opcache_compile_file($filePath)) { 138 | file_put_contents($filePath, "compile($rootDirectory . DIRECTORY_SEPARATOR . $dirChild, false); 154 | } 155 | } 156 | 157 | if ($isRootDir) { 158 | echo "Compilation has been successfully completed!\n"; 159 | exit(0); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/CactusErrorHandler.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/sampleProject/tests/CactusTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(str_contains($phpFileContents, "Cactus")); 15 | $this->assertTrue(str_contains($txtFileContents, "txt file")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/sampleProject/toBeCompiled/shouldBeCompiled.php: -------------------------------------------------------------------------------- 1 |