# Sysapi Bridge

Sysapi Bridge, Onoxsoft Panel'in Laravel uygulama katmanı ile işletim sistemi arasındaki güvenli arabirimi sağlar. Panel'in gerçek işlevselliğinin kalbidir — mock değil, gerçek OS entegrasyonu.

## Mimari Genel Bakış

```
┌─────────────────────────────────────────────────────┐
│               Laravel Application                    │
│  Controllers → Services → SysapiManager             │
└────────────────────┬────────────────────────────────┘
                     │ PHP Process::run()
                     ▼
┌─────────────────────────────────────────────────────┐
│            Sysapi Bridge Layer                       │
│  /srv/panel/scripts/sysapi/*.sh                     │
│  Sudo whitelist (/etc/sudoers.d/onox-panel)         │
└────────────────────┬────────────────────────────────┘
                     │ Shell exec (whitelisted only)
                     ▼
┌─────────────────────────────────────────────────────┐
│              Operating System                        │
│  Apache, PHP-FPM, MariaDB, PowerDNS, Postfix        │
│  Dovecot, Rspamd, certbot, fail2ban, useradd...     │
└─────────────────────────────────────────────────────┘
```

## Tasarım İlkeleri

1. **Her işlemin bir betiği var.** `account_create.sh`, `domain_add.sh`, `ssl_issue.sh` gibi atomik betikler.
2. **Laravel hiçbir zaman doğrudan `shell_exec()` kullanmaz.** Her şey `SysapiManager::run()` üzerinden geçer.
3. **Sudo whitelist statik.** Yalnızca bilinen betikler root yetkisiyle çalışabilir. Dinamik komut enjeksiyonu imkansız.
4. **İdempotent betikler.** İkinci kez çalıştırıldığında aynı sonucu verir, hata fırlatmaz.
5. **JSON çıktı standardı.** Her betik `{"success": true, "data": {...}}` veya `{"success": false, "error": "..."}` döner.

## Dizin Yapısı

```
/srv/panel/scripts/sysapi/
├── account/
│   ├── create.sh
│   ├── suspend.sh
│   ├── unsuspend.sh
│   ├── terminate.sh
│   └── chpasswd.sh
├── domain/
│   ├── add.sh
│   ├── remove.sh
│   ├── add_subdomain.sh
│   └── remove_subdomain.sh
├── dns/
│   ├── zone_create.sh
│   ├── zone_delete.sh
│   ├── record_add.sh
│   └── record_remove.sh
├── ssl/
│   ├── issue.sh          # certbot --nginx veya --apache
│   ├── revoke.sh
│   ├── autossl_cron.sh
│   └── install_custom.sh
├── mail/
│   ├── account_create.sh
│   ├── account_delete.sh
│   ├── account_chpasswd.sh
│   ├── dkim_generate.sh
│   └── queue_flush.sh
├── db/
│   ├── create.sh
│   ├── drop.sh
│   ├── create_user.sh
│   └── grant.sh
├── php/
│   ├── set_version.sh
│   └── list_versions.sh
├── backup/
│   ├── full_backup.sh
│   └── restore.sh
└── system/
    ├── service_status.sh
    ├── service_restart.sh
    ├── disk_usage.sh
    └── load_avg.sh
```

## Sudo Whitelist

`/etc/sudoers.d/onox-panel` dosyası:

```sudoers
# Onoxsoft Panel Sysapi Bridge — DO NOT EDIT MANUALLY
# Generated by: php artisan onox:generate-sudoers
Defaults:www-data !requiretty

www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/account/create.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/account/suspend.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/account/unsuspend.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/account/terminate.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/domain/add.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/domain/remove.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/ssl/issue.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/ssl/revoke.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/mail/account_create.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/mail/account_delete.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/db/create.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/db/drop.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/php/set_version.sh *
www-data ALL=(root) NOPASSWD: /srv/panel/scripts/sysapi/system/service_restart.sh *
```

Sudoers dosyasını yeniden üretmek için:

```bash
php artisan onox:generate-sudoers --dry-run  # önce kontrol et
php artisan onox:generate-sudoers            # yaz ve reload et
```

## SysapiManager — PHP Arayüzü

`app/Services/SysapiManager.php`:

```php
namespace App\Services;

use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
use App\Exceptions\SysapiException;

class SysapiManager
{
    private string $scriptsPath;

    public function __construct()
    {
        $this->scriptsPath = config('onox.sysapi.scripts_path',
            '/srv/panel/scripts/sysapi');
    }

    /**
     * Belirtilen Sysapi betiğini çalıştır.
     *
     * @param  string   $script   Göreli betik yolu (örn. "account/create")
     * @param  array    $args     Sıralı argümanlar (otomatik escape edilir)
     * @param  int      $timeout  Saniye cinsinden zaman aşımı
     * @return array              Betikten dönen JSON verisi
     * @throws SysapiException
     */
    public function run(string $script, array $args = [], int $timeout = 60): array
    {
        $scriptPath = $this->scriptsPath . '/' . ltrim($script, '/') . '.sh';

        if (!file_exists($scriptPath)) {
            throw new SysapiException("Script not found: {$scriptPath}");
        }

        $escapedArgs = array_map('escapeshellarg', $args);
        $command = 'sudo ' . escapeshellarg($scriptPath) . ' ' . implode(' ', $escapedArgs);

        $result = Process::timeout($timeout)->run($command);

        if (!$result->successful()) {
            $error = trim($result->errorOutput()) ?: "Exit code: {$result->exitCode()}";
            throw new SysapiException("Sysapi failed [{$script}]: {$error}");
        }

        $decoded = json_decode($result->output(), true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new SysapiException("Sysapi returned invalid JSON [{$script}]");
        }

        if (!($decoded['success'] ?? false)) {
            throw new SysapiException(
                $decoded['error'] ?? 'Unknown sysapi error',
                $decoded['code'] ?? 0
            );
        }

        return $decoded['data'] ?? [];
    }

    /**
     * Sonuç beklenmeksizin çalıştır (arka plan görev için).
     */
    public function runBackground(string $script, array $args = []): void
    {
        $this->run($script, $args, timeout: 5);
        // Veya queue job dispatch et
    }
}
```

## Örnek: Hesap Oluşturma Betiği

`scripts/sysapi/account/create.sh`:

```bash
#!/usr/bin/env bash
# Onoxsoft Sysapi — account/create.sh
# Kullanım: create.sh <username> <password> <domain> <php_version> <quota_mb>
set -euo pipefail

USERNAME="$1"
PASSWORD="$2"
DOMAIN="$3"
PHP_VERSION="${4:-8.3}"
QUOTA_MB="${5:-5120}"

HOMEDIR="/home/$USERNAME"
WEBROOT="$HOMEDIR/public_html"

# --- Validasyon ---
if id "$USERNAME" &>/dev/null; then
    echo '{"success":false,"error":"User already exists","code":"USER_EXISTS"}'
    exit 0
fi

# --- Sistem kullanıcısı oluştur ---
useradd -m -d "$HOMEDIR" -s /bin/bash "$USERNAME"
echo "$USERNAME:$PASSWORD" | chpasswd

# --- Dizin yapısı ---
mkdir -p "$WEBROOT" "$HOMEDIR/logs" "$HOMEDIR/tmp" "$HOMEDIR/.ssh"
chown -R "$USERNAME:$USERNAME" "$HOMEDIR"
chmod 711 "$HOMEDIR"
chmod 755 "$WEBROOT"

# --- Disk kotası ---
setquota -u "$USERNAME" "$QUOTA_MB" "$((QUOTA_MB + 1024))" 0 0 /

# --- PHP-FPM havuzu ---
cat > "/etc/php/${PHP_VERSION}/fpm/pool.d/${USERNAME}.conf" <<EOF
[$USERNAME]
user = $USERNAME
group = $USERNAME
listen = /run/php/php${PHP_VERSION}-fpm-${USERNAME}.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
slowlog = $HOMEDIR/logs/php-slow.log
php_admin_value[open_basedir] = $HOMEDIR:/tmp
php_admin_value[upload_tmp_dir] = $HOMEDIR/tmp
php_admin_value[session.save_path] = $HOMEDIR/tmp
EOF

systemctl reload "php${PHP_VERSION}-fpm"

# --- Apache vhost ---
cat > "/etc/httpd/conf.d/vhosts/${DOMAIN}.conf" <<EOF
<VirtualHost *:80>
    ServerName $DOMAIN
    ServerAlias www.$DOMAIN
    DocumentRoot $WEBROOT
    SuexecUserGroup $USERNAME $USERNAME

    <Directory $WEBROOT>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog $HOMEDIR/logs/error.log
    CustomLog $HOMEDIR/logs/access.log combined

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php${PHP_VERSION}-fpm-${USERNAME}.sock|fcgi://localhost"
    </FilesMatch>
</VirtualHost>
EOF

systemctl reload httpd

echo "{\"success\":true,\"data\":{\"username\":\"$USERNAME\",\"homedir\":\"$HOMEDIR\",\"webroot\":\"$WEBROOT\",\"php_version\":\"$PHP_VERSION\"}}"
```

## Örnek: Service Katmanında Kullanım

`app/Services/AccountService.php`:

```php
namespace App\Services;

use App\Models\Account;
use App\Services\SysapiManager;
use Illuminate\Support\Str;

class AccountService
{
    public function __construct(
        private SysapiManager $sysapi
    ) {}

    public function createAccount(array $data): Account
    {
        // 1. Sysapi ile OS'ta hesabı oluştur
        $osData = $this->sysapi->run('account/create', [
            $data['username'],
            $data['password'],
            $data['domain'],
            $data['php_version'] ?? '8.3',
            $data['quota_mb'] ?? 5120,
        ]);

        // 2. Veritabanına kaydet
        $account = Account::create([
            'username'   => $data['username'],
            'email'      => $data['email'],
            'domain'     => $data['domain'],
            'package_id' => $data['package_id'],
            'homedir'    => $osData['homedir'],
            'status'     => 'active',
        ]);

        // 3. DNS zonesi oluştur
        $this->sysapi->run('dns/zone_create', [
            $data['domain'],
            config('onox.server.ip'),
        ]);

        // 4. AutoSSL kuyruğuna ekle
        dispatch(new \App\Jobs\IssueAutoSSL($account));

        return $account;
    }

    public function suspendAccount(Account $account, string $reason = ''): void
    {
        $this->sysapi->run('account/suspend', [$account->username]);
        $account->update(['status' => 'suspended', 'suspend_reason' => $reason]);
    }
}
```

## Hata Yönetimi

```php
use App\Exceptions\SysapiException;

try {
    $result = $sysapi->run('account/create', $args);
} catch (SysapiException $e) {
    // $e->getMessage() — insan okunabilir hata
    // $e->getCode()    — makine kodu (USER_EXISTS, QUOTA_ERROR vb.)
    Log::error('Sysapi failed', [
        'script' => 'account/create',
        'error'  => $e->getMessage(),
        'code'   => $e->getCode(),
    ]);

    throw new \App\Exceptions\AccountCreationException($e->getMessage());
}
```

## Yeni Betik Ekleme

1. Betiği `scripts/sysapi/<kategori>/<eylem>.sh` konumuna yaz
2. İzinleri ayarla: `chmod 750 script.sh && chown root:www-data script.sh`
3. Sudoers satırını ekle: `php artisan onox:generate-sudoers`
4. Laravel servisinde `$sysapi->run('<kategori>/<eylem>', [...])` ile çağır
5. İdempotency testi: betiği iki kez çalıştır, ikincisi sessizce başarılı olmalı

## Test Ortamında Sysapi

Geliştirme ortamında gerçek OS değişikliği yapmadan test etmek için:

```php
// config/onox.php
'sysapi' => [
    'driver' => env('SYSAPI_DRIVER', 'real'), // 'real' | 'mock' | 'log'
],
```

`SYSAPI_DRIVER=mock` ile tüm `SysapiManager::run()` çağrıları mock JSON döner, gerçek betik çalışmaz.

```bash
# .env.testing
SYSAPI_DRIVER=mock
```

## Güvenlik Notları

- Betik argümanları PHP tarafında `escapeshellarg()` ile escape edilir
- Betikler içinde de argümanlar çift tırnak içinde kullanılır (`"$1"`)
- Betikler `set -euo pipefail` ile başlar — hata anında durur
- Sudo whitelist değişikliği için `visudo -c` doğrulama zorunludur
- Betik dosyaları `root:www-data 750` izniyle korunur — web saldırısıyla değiştirilemez
