Compare commits
6 Commits
6afd3840ec
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fc17765f6 | |||
| 80d080e2d6 | |||
| b416bf5061 | |||
| becbbe2be4 | |||
| 1c80ab98fc | |||
| 1b94624a6d |
@@ -1,68 +0,0 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "SITE_ROMFAST_2025"
|
||||
18
CLAUDE.md
18
CLAUDE.md
@@ -34,6 +34,23 @@ Directorul `efactura-generator/` din acest repo este o **oglindă** a proiectulu
|
||||
|
||||
`config.json` de pe server (`~/public_html/efactura-generator/config.json`) conține `api_key` și nu e nici în repo, nici în sync — se gestionează manual pe a2hosting.
|
||||
|
||||
## Sub-proiectul `oauth2/`
|
||||
|
||||
Flux OAuth2 ANAF eFactura (obținere access/refresh token) folosit de aplicația desktop ROA/ROACONT. Deployat pe `https://romfast.ro/oauth2/`.
|
||||
|
||||
- **`index.php`** — endpoint principal, trei ramuri:
|
||||
1. **Refresh token** (`refresh_token` primit prin `$_POST` sau `$_GET`) → passthrough curl către ANAF, întoarce JSON brut (folosit de butonul "Actualizare" din ROA).
|
||||
2. **Intrare inițială** (fără `?code`) → redirect 302 către ANAF authorize. Dacă vine cu `?state=<64 hex>` salvează state-ul în sesiunea PHP (flux automat, legat de `pick.php`); fără `state` = flux manual/retro-compat (tokenurile apar direct în pagină).
|
||||
3. **Callback ANAF** (`?code=...` sau `?error=...`) → schimb code→token; dacă există state valid în sesiune scrie fișier în `tokens/<sha256(state)>.json` (succes sau eroare) și afișează doar o pagină de confirmare; altfel (retro-compat) afișează access/refresh token direct în HTML.
|
||||
- **`pick.php`** — endpoint de polling apelat de ROA cu `POST state=<64 hex>`. Răspunde `{"status":"pending"}` până apare fișierul de token, apoi îl livrează o singură dată (claim atomic prin `rename`) și îl șterge. TTL 600s pentru fișiere orfane/`.tmp`.
|
||||
- **`tokens/`** — director de stocare temporară token/eroare, cheie = SHA-256(state). Protejat cu `.htaccess` (`Require all denied`); creat automat de `index.php`/`pick.php` dacă lipsește. Nu se commite conținutul (fișiere efemere, curățate de TTL).
|
||||
- **`php.ini`** — config logging erori local pentru acest subdirector (`error_log = /oauth2/error_log.txt`).
|
||||
- **`tests/run_tests.php`** — suită de teste self-contained (fără framework extern), acoperă `index.php` + `pick.php`. Rulare:
|
||||
```bash
|
||||
php oauth2/tests/run_tests.php
|
||||
```
|
||||
Ce face: copiază `index.php`/`pick.php` într-un director temporar (nu atinge `tokens/` din repo), pornește `php -S 127.0.0.1:8317` pe copie cu `session.save_path` propriu, rulează matricea de teste prin cereri HTTP reale (fără dependențe externe), oprește serverul și curăță totul la final (`register_shutdown_function`). Exit code 0 = toate testele au trecut; ieșirea arată `OK`/`FAIL` per test plus un rezumat `N trecute, M eșuate`. Notă: testele de refresh declanșează un apel curl real către `logincert.anaf.ro` cu un refresh_token fictiv (ANAF răspunde `invalid_grant`) — asercțiunile (nu e 302, `Content-Type` JSON) rămân valabile și offline.
|
||||
|
||||
## Development
|
||||
|
||||
No build process. Serve locally with:
|
||||
@@ -85,6 +102,7 @@ The navbar HTML is **manually duplicated** across every page (no templating). Wh
|
||||
- `/menu/` — company pages (about, contact, support, services, jobs, references)
|
||||
- `/roa/` — ROA ERP product pages (one per module)
|
||||
- `/efactura-generator/` — standalone Romanian e-invoice editor (loads XML, edits, prints, exports XML)
|
||||
- `/oauth2/` — ANAF eFactura OAuth2 token flow for the ROA desktop app (see "Sub-proiectul `oauth2/`" above)
|
||||
|
||||
### efactura-generator
|
||||
|
||||
|
||||
2
_arhiva_oauth2/.htaccess
Normal file
2
_arhiva_oauth2/.htaccess
Normal file
@@ -0,0 +1,2 @@
|
||||
# Interzice accesul HTTP la fisierele arhivate (belt-and-braces, langa extra-docroot)
|
||||
Require all denied
|
||||
@@ -151,6 +151,13 @@
|
||||
<span class="link-arrow mt-4 inline-block">Deschide aplicația →</span>
|
||||
</a>
|
||||
|
||||
<a href="https://autopass.romfast.ro" target="_blank" rel="noopener" class="professional-card" style="padding: 26px 24px;">
|
||||
<span class="feature-icon"><i data-lucide="car" class="w-5 h-5" style="color:var(--primary)"></i></span>
|
||||
<span class="feature-title mt-4 inline-block">ROA AutoPass</span>
|
||||
<p class="feature-desc mt-2">Declari prestațiile de service auto la RAR din câteva click-uri — încarci Excel/CSV, restul se face automat.</p>
|
||||
<span class="link-arrow mt-4 inline-block">Deschide aplicația →</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="hero-sub mx-auto text-center mt-10" style="font-size:15px;">
|
||||
@@ -171,6 +178,7 @@
|
||||
<a href="https://pdfqr.roa.romfast.ro" target="_blank" rel="noopener" title="Generează cod QR și-l adaugă la PDF" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="scan-line" class="w-4 h-4"></i>PDF QR</a>
|
||||
<a href="https://city-discovery.roa.romfast.ro" target="_blank" rel="noopener" title="Joc City Adventure Game" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="gamepad-2" class="w-4 h-4"></i>City Discovery</a>
|
||||
<a href="https://space.roa.romfast.ro" target="_blank" rel="noopener" title="Rezervări spații" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="calendar-check" class="w-4 h-4"></i>Space Booking</a>
|
||||
<a href="https://autopass.romfast.ro" target="_blank" rel="noopener" title="Declarații RAR pentru service-uri auto" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="car" class="w-4 h-4"></i>ROA AutoPass</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
421
oauth2/index.php
421
oauth2/index.php
@@ -1,136 +1,387 @@
|
||||
<?php
|
||||
// oAuth2 JWT Token
|
||||
|
||||
$get = $_REQUEST;
|
||||
$query_string = '';
|
||||
foreach ($get as $key => $value) {
|
||||
$query_string .= $key . '=' . $value . '&';
|
||||
}
|
||||
// echo($query_string);
|
||||
parse_str($query_string);
|
||||
// index.php — OAuth2 ANAF eFactura: obtinere token automat + compatibilitate retro
|
||||
// FR-1 (propagare state), FR-2 (sesiune primara), FR-3 (stocare tokenuri),
|
||||
// FR-4 (retro-compat fara state), FR-10 (securitate), FR-11 (protectie stocare)
|
||||
//
|
||||
// Modificat: 2026-07-07 — implementare flux automat (stocare fisier + pagina succes)
|
||||
|
||||
// === Credentiale ANAF (pastrate din versiunea anterioara) ===
|
||||
$client_id = 'f4c59081e488ac326f32f8ab52620023e996dd82d7d01163';
|
||||
$client_secret = 'e65b9127acac254c5b9bfbbe68b9a6ca07b0d6df6dbb0023e996dd82d7d01163';
|
||||
$redirect_uri = 'https://romfast.ro/oauth2/';
|
||||
|
||||
// REFRESH TOKEN se apeleaza din ROA cu parametrul ?refresh_token=REFRESH_TOKEN pentru prelungirea valabilitatii tokenului
|
||||
// === Director tokenuri (FR-11) ===
|
||||
// oauth2/tokens/ — protejat explicit cu .htaccess (Require all denied), deoarece
|
||||
// se afla in interiorul docroot-ului (public_html/oauth2/tokens), nu deasupra lui.
|
||||
// Codul creaza directorul si .htaccess-ul daca lipsesc (ex. prima instalare)
|
||||
$tokens_dir = __DIR__ . '/tokens';
|
||||
if (!is_dir($tokens_dir)) {
|
||||
@mkdir($tokens_dir, 0700, true);
|
||||
}
|
||||
$tokens_htaccess = $tokens_dir . '/.htaccess';
|
||||
if (!file_exists($tokens_htaccess)) {
|
||||
@file_put_contents($tokens_htaccess, "Require all denied\n");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Functii helper
|
||||
// ===================================================================
|
||||
|
||||
// Validare format state: lungime fixa 64, doar [A-Za-z0-9] (protectie path traversal - NFR-1)
|
||||
function validState($state) {
|
||||
return is_string($state)
|
||||
&& strlen($state) === 64
|
||||
&& (bool)preg_match('/^[A-Za-z0-9]+$/', $state);
|
||||
}
|
||||
|
||||
// Afisare pagina HTML simpla (§3.3: UTF-8 cu diacritice, H1 mare scanabil,
|
||||
// font >=16px, contrast inalt, un singur job per pagina, fara linkuri/butoane)
|
||||
function showPage($page_title, $h1_text, $h1_color, $body_text) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo '<!DOCTYPE html>' . "\n";
|
||||
echo '<html lang="ro"><head>' . "\n";
|
||||
echo '<meta charset="utf-8">' . "\n";
|
||||
echo '<meta name="viewport" content="width=device-width,initial-scale=1">' . "\n";
|
||||
echo '<title>' . htmlspecialchars($page_title, ENT_QUOTES, 'UTF-8') . '</title>' . "\n";
|
||||
echo '<style>' . "\n";
|
||||
echo 'body{font-family:Arial,Helvetica,sans-serif;font-size:18px;margin:3em auto;' . "\n";
|
||||
echo ' max-width:640px;padding:0 1.5em;background:#f7f7f7;color:#222}' . "\n";
|
||||
echo 'h1{font-size:2em;color:' . $h1_color . ';margin-bottom:0.5em}' . "\n";
|
||||
echo 'p{font-size:1em;line-height:1.7;margin:0}' . "\n";
|
||||
echo '.brand{font-size:0.8em;color:#999;margin-bottom:2.5em}' . "\n";
|
||||
echo '</style>' . "\n";
|
||||
echo '</head><body>' . "\n";
|
||||
echo '<p class="brand">ROA Romfast</p>' . "\n";
|
||||
echo '<h1>' . htmlspecialchars($h1_text, ENT_QUOTES, 'UTF-8') . '</h1>' . "\n";
|
||||
echo '<p>' . htmlspecialchars($body_text, ENT_QUOTES, 'UTF-8') . '</p>' . "\n";
|
||||
echo '</body></html>' . "\n";
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Citire explicita parametri GET (FR-10: elimina parse_str care injecta variabile)
|
||||
// ===================================================================
|
||||
// refresh_token vine din ROA prin POST body (refreshToken - anaf_efactura.prg) sau GET;
|
||||
// originalul folosea $_REQUEST, deci accepta ambele - citirea doar din $_GET rupe butonul Actualizare
|
||||
$refresh_token = isset($_POST['refresh_token']) ? (string)$_POST['refresh_token']
|
||||
: (isset($_GET['refresh_token']) ? (string)$_GET['refresh_token'] : '');
|
||||
$code = isset($_GET['code']) ? (string)$_GET['code'] : '';
|
||||
$state_param = isset($_GET['state']) ? (string)$_GET['state'] : '';
|
||||
$error_param = isset($_GET['error']) ? (string)$_GET['error'] : '';
|
||||
$error_description = isset($_GET['error_description']) ? (string)$_GET['error_description'] : '';
|
||||
|
||||
// ===================================================================
|
||||
// Ramura REFRESH TOKEN — neatinsa functional (FR-4)
|
||||
// Apelata din ROA direct cu ?refresh_token=... pentru prelungirea tokenului
|
||||
// ===================================================================
|
||||
if (!empty($refresh_token)) {
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
|
||||
//The data you want to send via POST
|
||||
$fields = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'refresh_token' => $refresh_token,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'refresh_token'
|
||||
'grant_type' => 'refresh_token',
|
||||
];
|
||||
|
||||
//url-ify the data for the POST
|
||||
$fields_string = http_build_query($fields);
|
||||
|
||||
//open connection
|
||||
$ch = curl_init();
|
||||
|
||||
//set the url, number of POST vars, POST data
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
|
||||
|
||||
//So that curl_exec returns the contents of the cURL; rather than echoing it
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
//execute post
|
||||
$jsonobj = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// $arr = json_decode($jsonobj, true);
|
||||
|
||||
/* $json = json_encode($jsonobj, JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
// Avoid echo of empty string (which is invalid JSON), and
|
||||
// JSONify the error message instead:
|
||||
$json = json_encode(["error" => json_last_error_msg()]);
|
||||
if ($json === false) {
|
||||
// This should not happen, but we go all the way now:
|
||||
$json = '{"error":"unknown"}';
|
||||
}
|
||||
// Set HTTP response status code to: 500 - Internal Server Error
|
||||
http_response_code(500);
|
||||
}
|
||||
*/
|
||||
header("Content-Type: application/json");
|
||||
echo $jsonobj;
|
||||
|
||||
// Close handle
|
||||
//curl_close($ch);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
// La eroare curl: corp gol, identic cu comportamentul vechi (FR-4: ramura neatinsa)
|
||||
echo ($jsonobj !== false) ? $jsonobj : '';
|
||||
exit();
|
||||
}
|
||||
|
||||
// COD AUTORIZARE se apeleaza din ROA fara parametri pentru obtinerea codului de autorizare de la ANAF. ANAF apeleaza aceasta pagina (callback) cu codul de autorizare
|
||||
// ===================================================================
|
||||
// Configurare cookie de sesiune PHP (FR-2)
|
||||
// session_set_cookie_params TREBUIE sa preceada session_start pe AMBELE intrari:
|
||||
// - intrarea initiala (fara ?code): browser deschis de ROACONT
|
||||
// - callback-ul ANAF (cu ?code): navigare top-level cross-site
|
||||
//
|
||||
// SameSite=Lax este obligatoriu: callback-ul ANAF este o navigare top-level dintr-un
|
||||
// alt domeniu (logincert.anaf.ro -> romfast.ro) — cu SameSite=Strict cookie-ul nu ar
|
||||
// fi trimis in callback si legarea sesiune ar esua silentios.
|
||||
// ===================================================================
|
||||
if (PHP_VERSION_ID >= 70300) {
|
||||
// Sintaxa cu array: disponibila incepand cu PHP 7.3
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
} else {
|
||||
// Fallback PHP < 7.3: SameSite injectat ca sufix al parametrului path
|
||||
session_set_cookie_params(0, '/; SameSite=Lax', '', true, true);
|
||||
}
|
||||
session_start();
|
||||
|
||||
// ===================================================================
|
||||
// Callback de eroare ANAF (?error=...) — FR-10
|
||||
// Tratare explicita; nu se re-redirecteaza la authorize (ar produce bucla)
|
||||
// ===================================================================
|
||||
if (!empty($error_param)) {
|
||||
// Extrage state-ul din sesiune pentru a scrie fisierul de eroare
|
||||
$sess_state = null;
|
||||
if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) {
|
||||
$sess = $_SESSION['roa_state'];
|
||||
if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) {
|
||||
$sess_state = $sess['value'];
|
||||
}
|
||||
}
|
||||
unset($_SESSION['roa_state']);
|
||||
session_write_close();
|
||||
|
||||
// Scrie fisier de eroare pentru state-ul din sesiune (daca exista si valid)
|
||||
if ($sess_state !== null && validState($sess_state)) {
|
||||
$fname = hash('sha256', $sess_state);
|
||||
$token_file = $tokens_dir . '/' . $fname . '.json';
|
||||
$tmp_file = $tokens_dir . '/' . $fname . '.tmp';
|
||||
// Anti-suprascriere: nu suprascrie un fisier de succes existent (FR-10)
|
||||
if (!file_exists($token_file)) {
|
||||
$err_data = json_encode([
|
||||
'error' => $error_param,
|
||||
'error_description' => $error_description,
|
||||
]);
|
||||
if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) {
|
||||
@rename($tmp_file, $token_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagina de eroare (§3.3: un job, fara retry link, fara re-redirect)
|
||||
$cause = !empty($error_description) ? $error_description : $error_param;
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
$cause . ' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Intrare INITIALA — nicio sesiune, fara ?code: redirect catre ANAF authorize
|
||||
// ===================================================================
|
||||
if (empty($code)) {
|
||||
// Obtinere cod autorizare
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize';
|
||||
$url .='?client_id='.$client_id;
|
||||
$url .='&client_secret='.$client_secret;
|
||||
$url .='&response_type=code';
|
||||
$url .='&redirect_uri='.$redirect_uri;
|
||||
$url .='&token_content_type=jwt';
|
||||
|
||||
|
||||
// Redirectionez la pagina de autorizare ANAF pentru obtinerea codului de autorizare
|
||||
// echo $url;
|
||||
header('Location: '.$url);
|
||||
if (!empty($state_param)) {
|
||||
// Validare format state inainte de orice operatie (protectie path traversal)
|
||||
if (!validState($state_param)) {
|
||||
session_write_close();
|
||||
// L1: H1 conform §3.3 ("Autorizarea nu a reușit" pentru toate paginile de eroare)
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
'Parametru de sesiune invalid. Reluați operația din aplicația ROA.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
// NEW TOKEN
|
||||
// callback ANAF: https://romfast.ro/oauth2/?code=COD_AUTORIZARE
|
||||
|
||||
// Obtinere token pe baza codului de autorizare
|
||||
// print_r('Codul de autorizare este: '.$code);
|
||||
// Salveaza state in sesiune cu timestamp (mecanism PRIMAR de legare)
|
||||
$_SESSION['roa_state'] = ['value' => $state_param, 'ts' => time()];
|
||||
session_write_close();
|
||||
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
} else {
|
||||
// FR-2 igienă sesiune: fara state in URL = flux manual/retro-compat
|
||||
// Curata orice roa_state ramas dintr-o tentativa automata anterioara esuata
|
||||
// (altfel, la callback, am afisa pagina de succes fara tokenuri = fund de sac)
|
||||
unset($_SESSION['roa_state']);
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
//The data you want to send via POST
|
||||
$fields = [
|
||||
// URL authorize ANAF — client_secret NU se trimite la authorize (FR-10)
|
||||
// client_secret apartine doar schimbului code->token (POST, fara URL)
|
||||
$url_auth = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize';
|
||||
$url_auth .= '?client_id=' . urlencode($client_id);
|
||||
$url_auth .= '&response_type=code';
|
||||
$url_auth .= '&redirect_uri=' . urlencode($redirect_uri);
|
||||
$url_auth .= '&token_content_type=jwt';
|
||||
// FR-1: propagare state la ANAF daca exista
|
||||
// Nota: doc ANAF (pag.23) spune "State se lasa necompletat" — propagarea e incerta
|
||||
// dar trimitem oricum; legarea primara ramane sesiunea PHP
|
||||
if (!empty($state_param)) {
|
||||
$url_auth .= '&state=' . urlencode($state_param);
|
||||
}
|
||||
|
||||
header('Location: ' . $url_auth);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Callback cu cod de autorizare (?code=...) — schimb code -> token
|
||||
// ===================================================================
|
||||
|
||||
// FR-2: identifica state-ul din sesiune (mecanismul PRIMAR de legare)
|
||||
// state-ul din sesiune = cel generat de ROACONT si salvat la intrarea initiala
|
||||
$sess_state = null;
|
||||
if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) {
|
||||
$sess = $_SESSION['roa_state'];
|
||||
if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) {
|
||||
// roa_state valid (sub 15 minute)
|
||||
$sess_state = $sess['value'];
|
||||
}
|
||||
// roa_state mai vechi de 15 minute se ignora (si se sterge mai jos)
|
||||
}
|
||||
// Consuma state-ul din sesiune (indiferent de validitate — evita re-utilizare)
|
||||
unset($_SESSION['roa_state']);
|
||||
|
||||
// FR-2: validare CSRF oportunistica
|
||||
// Daca ANAF a propagat state in callback (comportament incert), trebuie sa coincida cu sesiunea
|
||||
// Nepotrivire = posibil CSRF sau sesiune gresita -> eroare, fara stocare tokenuri
|
||||
if (!empty($state_param) && $sess_state !== null) {
|
||||
if ($state_param !== $sess_state) {
|
||||
session_write_close();
|
||||
showPage(
|
||||
'Eroare de securitate',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
'Identificatorul de sesiune nu corespunde. Operația a fost anulată din motive de securitate. ' .
|
||||
'Reveniți în aplicația ROA și reluați de acolo.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// FR-11 / C10: elibereaza lock-ul de sesiune inainte de curl
|
||||
// ANAF poate raspunde lent; nu tinem lock-ul de sesiune blocat in acest timp
|
||||
session_write_close();
|
||||
|
||||
// Schimb cod de autorizare -> token (FR-10: cu timeouts + tratare erori)
|
||||
$url_token = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
$fields_token = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'code' => $code,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'authorization_code',
|
||||
'token_content_type' => 'jwt'
|
||||
'token_content_type' => 'jwt',
|
||||
];
|
||||
$fields_string = http_build_query($fields_token);
|
||||
|
||||
//url-ify the data for the POST
|
||||
$fields_string = http_build_query($fields);
|
||||
|
||||
//open connection
|
||||
$ch = curl_init();
|
||||
|
||||
//set the url, number of POST vars, POST data
|
||||
curl_setopt($ch,CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_URL, $url_token);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
|
||||
|
||||
//So that curl_exec returns the contents of the cURL; rather than echoing it
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // FR-10: timeout conectare
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // FR-10: timeout total
|
||||
|
||||
//execute post
|
||||
$jsonobj = curl_exec($ch);
|
||||
//echo $jsonobj;
|
||||
|
||||
$arr = json_decode($jsonobj, true);
|
||||
|
||||
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
|
||||
echo '<p><b>Access Token:</b></p><p>'.$arr["access_token"].'</p>';
|
||||
|
||||
echo '<p><b>Refresh Token:</b></p><p>'.$arr["refresh_token"].'</p>';
|
||||
|
||||
// Close handle
|
||||
$curl_errno = curl_errno($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// FR-10: tratare explicita erori curl / HTTP != 200 / JSON de eroare
|
||||
// (inainte: un esec afisa tokenuri goale)
|
||||
$error_msg = null; // text uman (pagina browser + error_description)
|
||||
$err_code = null; // cod scurt ASCII citit de ROACONT (loJson.error -> M12)
|
||||
$token_data = null;
|
||||
|
||||
if ($curl_errno !== 0) {
|
||||
$err_code = 'curl_error';
|
||||
$error_msg = 'Eroare de rețea la contactarea ANAF (curl errno: ' . (int)$curl_errno . ').';
|
||||
} elseif ($http_code !== 200) {
|
||||
$err_code = 'http_error';
|
||||
$error_msg = 'ANAF a răspuns cu codul HTTP ' . (int)$http_code . '.';
|
||||
} else {
|
||||
$token_data = json_decode((string)$jsonobj, true);
|
||||
if ($token_data === null) {
|
||||
$err_code = 'json_invalid';
|
||||
$error_msg = 'Răspunsul ANAF nu a putut fi decodificat (JSON invalid).';
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$err_code = $token_data['error']; // cod brut ANAF: invalid_grant, access_denied etc.
|
||||
$err_desc = !empty($token_data['error_description']) ? $token_data['error_description'] : '';
|
||||
$error_msg = 'ANAF: ' . $err_code . ($err_desc ? ' — ' . $err_desc : '') . '.';
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Scriere fisier tokenuri (FR-3, FR-11)
|
||||
// Se efectueaza DOAR daca sesiunea avea un state valid (flux automat)
|
||||
// ===================================================================
|
||||
if ($sess_state !== null && validState($sess_state)) {
|
||||
$fname = hash('sha256', $sess_state);
|
||||
$token_file = $tokens_dir . '/' . $fname . '.json';
|
||||
$tmp_file = $tokens_dir . '/' . $fname . '.tmp';
|
||||
|
||||
?>
|
||||
if ($error_msg !== null) {
|
||||
// Eroare: scrie fisier de eroare
|
||||
// FR-10 anti-suprascriere: nu suprascrie un token de succes existent
|
||||
// (ex. refresh pe pagina callback dupa succes: code expirat -> eroare -> nu stergem tokenul bun)
|
||||
if (!file_exists($token_file)) {
|
||||
$err_data = json_encode([
|
||||
'error' => $err_code, // cod scurt ASCII citit de ROACONT (fara mojibake)
|
||||
'error_description' => $error_msg, // text uman UTF-8 (diagnostic)
|
||||
]);
|
||||
if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) {
|
||||
@rename($tmp_file, $token_file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Succes: scriere atomica a tokenurilor
|
||||
$json_out = json_encode($token_data, JSON_UNESCAPED_SLASHES);
|
||||
$write_ok = false;
|
||||
if ($json_out !== false && file_put_contents($tmp_file, $json_out) !== false) {
|
||||
// @rename() pe acelasi filesystem = atomic (pick.php nu poate citi JSON partial)
|
||||
if (@rename($tmp_file, $token_file)) {
|
||||
$write_ok = true;
|
||||
} else {
|
||||
@unlink($tmp_file); // curata .tmp la esec rename; TTL il curata oricum
|
||||
}
|
||||
}
|
||||
if (!$write_ok) {
|
||||
// Scriere esuata — afisam eroare (nu succes fals); ROACONT va detecta timeout
|
||||
$error_msg = 'Tokenul ANAF a fost primit dar nu a putut fi salvat temporar pe server. Reveniți în aplicație și reluați operația.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Afisare pagina rezultat (§3.3)
|
||||
// ===================================================================
|
||||
if ($sess_state !== null) {
|
||||
// Flux automat (state in sesiune): afiseaza succes sau eroare
|
||||
// Tokenurile NU apar in pagina — sunt ridicate de pick.php
|
||||
if ($error_msg !== null) {
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
$error_msg .
|
||||
' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.'
|
||||
);
|
||||
} else {
|
||||
// §3.3 pagina succes: H1 verde, corp cu instructiunea de revenire in ROA
|
||||
showPage(
|
||||
'Autorizare reușită',
|
||||
'Autorizare reușită',
|
||||
'#27ae60',
|
||||
'Reveniți în aplicația ROA — tokenul se preia automat. Puteți închide această pagină.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// FR-4 retro-compat: fara state in sesiune = versiune veche ROACONT sau sesiune expirata
|
||||
// Comportamentul ACTUAL ramas neschimbat: tokenurile apar in pagina pentru copiere manuala
|
||||
if ($error_msg !== null) {
|
||||
echo '<p>Eroare la obtinerea tokenului: ' . htmlspecialchars($error_msg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||
} else {
|
||||
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
|
||||
echo '<p><b>Access Token:</b></p><p>';
|
||||
echo htmlspecialchars((string)(isset($token_data['access_token']) ? $token_data['access_token'] : ''), ENT_QUOTES, 'UTF-8');
|
||||
echo '</p>';
|
||||
echo '<p><b>Refresh Token:</b></p><p>';
|
||||
echo htmlspecialchars((string)(isset($token_data['refresh_token']) ? $token_data['refresh_token'] : ''), ENT_QUOTES, 'UTF-8');
|
||||
echo '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
62
oauth2/pick.php
Normal file
62
oauth2/pick.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
// pick.php — Endpoint polling preluare automata token OAuth2 ANAF eFactura
|
||||
// Apelat de ROACONT cu POST state=<64 hex>; raspunde JSON:
|
||||
// {"status":"pending"} cat timp tokenul nu a sosit (intotdeauna HTTP 200, nu 404),
|
||||
// continutul fisierului de token (succes sau eroare ANAF) o SINGURA data, apoi il sterge.
|
||||
// Fara session_start() — evita serializarea pe lock-ul de sesiune PHP.
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Director tokenuri (acelasi ca in index.php) — protejat cu .htaccess
|
||||
$tokens_dir = __DIR__ . '/tokens';
|
||||
if (!is_dir($tokens_dir)) {
|
||||
@mkdir($tokens_dir, 0700, true);
|
||||
}
|
||||
$tokens_htaccess = $tokens_dir . '/.htaccess';
|
||||
if (!file_exists($tokens_htaccess)) {
|
||||
@file_put_contents($tokens_htaccess, "Require all denied\n");
|
||||
}
|
||||
|
||||
// Validare stricta format state: 64 caractere [A-Za-z0-9] (protectie path traversal)
|
||||
$state = isset($_POST['state']) ? (string)$_POST['state'] : '';
|
||||
if (strlen($state) !== 64 || !preg_match('/^[A-Za-z0-9]+$/', $state)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_state']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// TTL: sterge fisierele de tokenuri (si .tmp orfane) mai vechi de 10 minute
|
||||
// (aliniat cu timeout-ul de polling din ROACONT)
|
||||
$now = time();
|
||||
foreach (array_merge((array)glob($tokens_dir . '/*.json'), (array)glob($tokens_dir . '/*.tmp')) as $f) {
|
||||
if (($now - filemtime($f)) > 600) {
|
||||
@unlink($f);
|
||||
}
|
||||
}
|
||||
|
||||
// Numele fisierului = SHA-256(state): secretul nu apare in listinguri de directoare
|
||||
$token_file = $tokens_dir . '/' . hash('sha256', $state) . '.json';
|
||||
|
||||
if (!file_exists($token_file)) {
|
||||
echo json_encode(['status' => 'pending']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Claim atomic prin rename: din doua cereri concurente una singura primeste tokenul,
|
||||
// cealalta "pending"; previne si citirea unui JSON partial (index.php scrie .tmp + rename)
|
||||
$claim_file = $tokens_dir . '/' . hash('sha256', $state) . '_claim_' . getmypid() . '.json';
|
||||
if (!@rename($token_file, $claim_file)) {
|
||||
echo json_encode(['status' => 'pending']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Citire INAINTE de stergere; la esec nu stergem (TTL curata fisierul claim ramas)
|
||||
$content = file_get_contents($claim_file);
|
||||
if ($content === false || $content === '') {
|
||||
echo json_encode(['error' => 'read_error']);
|
||||
exit();
|
||||
}
|
||||
@unlink($claim_file);
|
||||
|
||||
// Continutul JSON exact cum a fost salvat de index.php (tokenuri sau eroare ANAF)
|
||||
echo $content;
|
||||
322
oauth2/tests/run_tests.php
Normal file
322
oauth2/tests/run_tests.php
Normal file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
// run_tests.php — teste pentru oauth2/index.php + oauth2/pick.php (flux automat token ANAF)
|
||||
//
|
||||
// Rulare: php oauth2/tests/run_tests.php
|
||||
//
|
||||
// Nu exista framework de teste in proiect; scriptul este self-contained:
|
||||
// - copiaza index.php si pick.php intr-un director temporar (nu atinge tokens/ din repo),
|
||||
// - porneste "php -S 127.0.0.1:8317" pe copie, cu sesiuni intr-un save_path propriu,
|
||||
// - ruleaza matricea de teste cu cereri HTTP (streams, fara dependinte externe),
|
||||
// - opreste serverul si curata directorul temporar; exit code 0 = toate au trecut.
|
||||
//
|
||||
// NOTA retea: testele de refresh (passthrough) declanseaza in index.php un apel curl
|
||||
// real catre logincert.anaf.ro cu un refresh_token fictiv (ANAF raspunde invalid_grant).
|
||||
// Asertiunile (nu e 302, Content-Type JSON) raman valabile si offline (corp gol).
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
const BASE = 'http://127.0.0.1:8317';
|
||||
|
||||
// ===================================================================
|
||||
// Helper HTTP: cerere fara follow-redirect, cu cookie optional
|
||||
// Returneaza ['status' => int, 'headers' => array, 'body' => string]
|
||||
// ===================================================================
|
||||
function http($method, $url, $post = null, $cookie = null, $timeout = 45) {
|
||||
$header = "Connection: close\r\n";
|
||||
if ($cookie !== null) {
|
||||
$header .= 'Cookie: ' . $cookie . "\r\n";
|
||||
}
|
||||
$opts = ['http' => [
|
||||
'method' => $method,
|
||||
'ignore_errors' => true, // returneaza corpul si la 4xx/5xx
|
||||
'follow_location' => 0, // vrem sa vedem 302-ul, nu sa-l urmam
|
||||
'timeout' => $timeout,
|
||||
]];
|
||||
if ($post !== null) {
|
||||
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
|
||||
$opts['http']['content'] = http_build_query($post);
|
||||
}
|
||||
$opts['http']['header'] = $header;
|
||||
$body = @file_get_contents($url, false, stream_context_create($opts));
|
||||
$headers = isset($http_response_header) ? $http_response_header : [];
|
||||
$status = 0;
|
||||
if (!empty($headers) && preg_match('#^HTTP/\S+\s+(\d{3})#', $headers[0], $m)) {
|
||||
$status = (int)$m[1];
|
||||
}
|
||||
return ['status' => $status, 'headers' => $headers, 'body' => (string)$body];
|
||||
}
|
||||
|
||||
// Cauta un header dupa nume (case-insensitive); null daca lipseste
|
||||
function hdr($resp, $name) {
|
||||
foreach ($resp['headers'] as $h) {
|
||||
if (stripos($h, $name . ':') === 0) {
|
||||
return trim(substr($h, strlen($name) + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Mod copil pentru testul de claim concurent:
|
||||
// php run_tests.php --pick-once <url> <state>
|
||||
// Face un singur POST pe pick.php si scrie corpul raspunsului pe stdout.
|
||||
// ===================================================================
|
||||
if (isset($argv[1]) && $argv[1] === '--pick-once') {
|
||||
$r = http('POST', $argv[2], ['state' => $argv[3]]);
|
||||
echo $r['body'];
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Setup: director temporar + server php -S
|
||||
// ===================================================================
|
||||
$tmp = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'roa_oauth2_tests_' . getmypid();
|
||||
$sessions = $tmp . DIRECTORY_SEPARATOR . 'sessions';
|
||||
$tokens = $tmp . DIRECTORY_SEPARATOR . 'tokens';
|
||||
if (!mkdir($tmp, 0700, true) || !mkdir($sessions, 0700, true)) {
|
||||
fwrite(STDERR, "Nu pot crea directorul temporar: $tmp\n");
|
||||
exit(2);
|
||||
}
|
||||
copy(__DIR__ . '/../index.php', $tmp . DIRECTORY_SEPARATOR . 'index.php');
|
||||
copy(__DIR__ . '/../pick.php', $tmp . DIRECTORY_SEPARATOR . 'pick.php');
|
||||
|
||||
$server = proc_open(
|
||||
[PHP_BINARY, '-S', '127.0.0.1:8317', '-t', $tmp,
|
||||
'-d', 'session.save_path=' . $sessions,
|
||||
'-d', 'xdebug.mode=off'],
|
||||
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
||||
$pipes
|
||||
);
|
||||
if (!is_resource($server)) {
|
||||
fwrite(STDERR, "Nu pot porni php -S\n");
|
||||
exit(2);
|
||||
}
|
||||
register_shutdown_function(function () use ($server, $tmp) {
|
||||
@proc_terminate($server);
|
||||
@proc_close($server);
|
||||
// curatare best-effort a directorului temporar
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($tmp, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($it as $f) {
|
||||
$f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname());
|
||||
}
|
||||
@rmdir($tmp);
|
||||
});
|
||||
|
||||
// Asteapta serverul (max 5 secunde)
|
||||
$ready = false;
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$s = @fsockopen('127.0.0.1', 8317, $en, $es, 0.1);
|
||||
if ($s) { fclose($s); $ready = true; break; }
|
||||
usleep(100000);
|
||||
}
|
||||
if (!$ready) {
|
||||
fwrite(STDERR, "Serverul php -S nu a pornit pe 127.0.0.1:8317\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Asertiuni
|
||||
// ===================================================================
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
function check($name, $cond, $detail = '') {
|
||||
global $pass, $fail;
|
||||
if ($cond) {
|
||||
$pass++;
|
||||
echo " OK $name\n";
|
||||
} else {
|
||||
$fail++;
|
||||
echo " FAIL $name" . ($detail !== '' ? " [$detail]" : '') . "\n";
|
||||
}
|
||||
}
|
||||
function state_nou() {
|
||||
return bin2hex(random_bytes(32)); // 64 caractere hex = format valid
|
||||
}
|
||||
function fisier_token($tokens, $state) {
|
||||
return $tokens . DIRECTORY_SEPARATOR . hash('sha256', $state) . '.json';
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// pick.php
|
||||
// ===================================================================
|
||||
echo "pick.php\n";
|
||||
|
||||
// P1: fara state -> 400 invalid_state
|
||||
$r = http('POST', BASE . '/pick.php', []);
|
||||
check('P1 fara state -> 400 invalid_state',
|
||||
$r['status'] === 400 && strpos($r['body'], 'invalid_state') !== false,
|
||||
"status={$r['status']} body={$r['body']}");
|
||||
|
||||
// P2: state prea scurt -> 400
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => 'abc123']);
|
||||
check('P2 state prea scurt -> 400', $r['status'] === 400, "status={$r['status']}");
|
||||
|
||||
// P3: lungime 64 dar caractere invalide (path traversal) -> 400
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => str_repeat('a', 60) . '/../']);
|
||||
check('P3 caractere invalide -> 400', $r['status'] === 400, "status={$r['status']}");
|
||||
|
||||
// P4: state valid, fisier inexistent -> 200 pending
|
||||
$s = state_nou();
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
check('P4 fara fisier -> 200 {"status":"pending"}',
|
||||
$r['status'] === 200 && $r['body'] === '{"status":"pending"}',
|
||||
"status={$r['status']} body={$r['body']}");
|
||||
check('P4 Content-Type JSON',
|
||||
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false);
|
||||
|
||||
// FR-11: prima cerere a creat tokens/ + .htaccess "Require all denied"
|
||||
check('FR-11 tokens/.htaccess creat automat',
|
||||
is_file($tokens . '/.htaccess')
|
||||
&& strpos((string)file_get_contents($tokens . '/.htaccess'), 'Require all denied') !== false);
|
||||
|
||||
// P5: fisier prezent -> continutul exact, O SINGURA data, apoi pending
|
||||
$s = state_nou();
|
||||
$continut = '{"access_token":"AT_test","refresh_token":"RT_test","expires_in":3600}';
|
||||
file_put_contents(fisier_token($tokens, $s), $continut);
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
check('P5 fisier prezent -> continutul exact', $r['body'] === $continut,
|
||||
"body={$r['body']}");
|
||||
check('P5 fisierul e sters dupa livrare', !file_exists(fisier_token($tokens, $s)));
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
check('P5 a doua cerere -> pending', $r['body'] === '{"status":"pending"}',
|
||||
"body={$r['body']}");
|
||||
|
||||
// P6: TTL — fisier mai vechi de 10 minute -> sters, raspuns pending
|
||||
$s = state_nou();
|
||||
file_put_contents(fisier_token($tokens, $s), $continut);
|
||||
touch(fisier_token($tokens, $s), time() - 700);
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
check('P6 fisier expirat (TTL 600s) -> pending', $r['body'] === '{"status":"pending"}',
|
||||
"body={$r['body']}");
|
||||
check('P6 fisierul expirat e sters', !file_exists(fisier_token($tokens, $s)));
|
||||
|
||||
// P7: TTL curata si .tmp orfane vechi
|
||||
$tmp_orfan = $tokens . DIRECTORY_SEPARATOR . 'orfan.tmp';
|
||||
file_put_contents($tmp_orfan, 'x');
|
||||
touch($tmp_orfan, time() - 700);
|
||||
http('POST', BASE . '/pick.php', ['state' => state_nou()]);
|
||||
check('P7 .tmp orfan vechi e sters de TTL', !file_exists($tmp_orfan));
|
||||
|
||||
// P8: claim concurent — doua cereri simultane, exact una primeste tokenul.
|
||||
// Nota: php -S pe Windows serveste secvential; concurenta reala e pe partea de
|
||||
// client, dar mecanismul testat (rename atomic in pick.php) acopera ambele cazuri.
|
||||
$s = state_nou();
|
||||
file_put_contents(fisier_token($tokens, $s), $continut);
|
||||
$copii = [];
|
||||
$pipe = [];
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$copii[$i] = proc_open(
|
||||
[PHP_BINARY, '-d', 'xdebug.mode=off', __FILE__, '--pick-once', BASE . '/pick.php', $s],
|
||||
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
||||
$pipe[$i]
|
||||
);
|
||||
}
|
||||
$rasp = [];
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$rasp[$i] = stream_get_contents($pipe[$i][1]);
|
||||
proc_close($copii[$i]);
|
||||
}
|
||||
$cu_token = 0;
|
||||
$cu_pending = 0;
|
||||
foreach ($rasp as $b) {
|
||||
if ($b === $continut) $cu_token++;
|
||||
if ($b === '{"status":"pending"}') $cu_pending++;
|
||||
}
|
||||
check('P8 claim concurent: exact una primeste tokenul, cealalta pending',
|
||||
$cu_token === 1 && $cu_pending === 1,
|
||||
'raspunsuri: [' . implode('] [', $rasp) . ']');
|
||||
|
||||
// ===================================================================
|
||||
// index.php
|
||||
// ===================================================================
|
||||
echo "index.php\n";
|
||||
|
||||
// I1: fara parametri -> 302 catre authorize, FARA client_secret, fara state
|
||||
$r = http('GET', BASE . '/index.php');
|
||||
$loc = (string)hdr($r, 'Location');
|
||||
check('I1 fara parametri -> 302', $r['status'] === 302, "status={$r['status']}");
|
||||
check('I1 Location = ANAF authorize',
|
||||
strpos($loc, 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize?') === 0, "loc=$loc");
|
||||
check('I1 authorize contine response_type=code + jwt',
|
||||
strpos($loc, 'response_type=code') !== false
|
||||
&& strpos($loc, 'token_content_type=jwt') !== false);
|
||||
check('I1 client_secret NU apare in URL-ul authorize',
|
||||
strpos($loc, 'client_secret') === false, "loc=$loc");
|
||||
check('I1 fara state in URL cand nu s-a trimis state',
|
||||
strpos($loc, 'state=') === false, "loc=$loc");
|
||||
|
||||
// I2: ?state=valid -> 302 cu state propagat + cookie de sesiune
|
||||
$s = state_nou();
|
||||
$r = http('GET', BASE . '/index.php?state=' . $s);
|
||||
$loc = (string)hdr($r, 'Location');
|
||||
check('I2 ?state=valid -> 302 spre authorize', $r['status'] === 302
|
||||
&& strpos($loc, 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize?') === 0,
|
||||
"status={$r['status']}");
|
||||
check('I2 state propagat in authorize', strpos($loc, 'state=' . $s) !== false, "loc=$loc");
|
||||
check('I2 client_secret NU apare', strpos($loc, 'client_secret') === false);
|
||||
$setc = (string)hdr($r, 'Set-Cookie');
|
||||
check('I2 Set-Cookie sesiune PHP', strpos($setc, 'PHPSESSID=') !== false, "set-cookie=$setc");
|
||||
|
||||
// I3: ?state cu format invalid -> pagina de eroare, fara redirect
|
||||
$r = http('GET', BASE . '/index.php?state=' . str_repeat('a', 60) . '..%2F.');
|
||||
check('I3 state invalid -> 200 pagina eroare (nu redirect)',
|
||||
$r['status'] === 200 && hdr($r, 'Location') === null, "status={$r['status']}");
|
||||
check('I3 pagina contine "Autorizarea nu a reușit"',
|
||||
strpos($r['body'], 'Autorizarea nu a reușit') !== false);
|
||||
|
||||
// I4: refresh prin POST body -> passthrough JSON (regresia fixata in becbbe2:
|
||||
// inainte, POST-ul era ignorat si cererea cadea pe 302 authorize)
|
||||
$r = http('POST', BASE . '/index.php', ['refresh_token' => 'dummy_refresh_token_test']);
|
||||
check('I4 refresh POST -> nu e 302 (regresie buton Actualizare)',
|
||||
$r['status'] !== 302, "status={$r['status']}");
|
||||
check('I4 refresh POST -> Content-Type application/json',
|
||||
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false,
|
||||
'content-type=' . hdr($r, 'Content-Type'));
|
||||
|
||||
// I5: refresh prin GET (compatibilitate retro) -> acelasi passthrough
|
||||
$r = http('GET', BASE . '/index.php?refresh_token=dummy_refresh_token_test');
|
||||
check('I5 refresh GET -> nu e 302', $r['status'] !== 302, "status={$r['status']}");
|
||||
check('I5 refresh GET -> Content-Type application/json',
|
||||
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false,
|
||||
'content-type=' . hdr($r, 'Content-Type'));
|
||||
|
||||
// I6: callback ?error= fara sesiune -> pagina de eroare, fara fisier scris
|
||||
$inainte = count((array)glob($tokens . '/*.json'));
|
||||
$r = http('GET', BASE . '/index.php?error=access_denied&error_description=Test');
|
||||
check('I6 ?error= fara sesiune -> pagina eroare',
|
||||
$r['status'] === 200 && strpos($r['body'], 'Autorizarea nu a reușit') !== false,
|
||||
"status={$r['status']}");
|
||||
check('I6 nu se scrie fisier de token fara sesiune',
|
||||
count((array)glob($tokens . '/*.json')) === $inainte);
|
||||
|
||||
// I7: flux complet de eroare: intrare cu state (sesiune) -> callback ?error=
|
||||
// -> fisier de eroare scris -> pick.php il livreaza o singura data
|
||||
$s = state_nou();
|
||||
$r = http('GET', BASE . '/index.php?state=' . $s);
|
||||
$cookie = null;
|
||||
if (preg_match('/PHPSESSID=([^;]+)/', (string)hdr($r, 'Set-Cookie'), $m)) {
|
||||
$cookie = 'PHPSESSID=' . $m[1];
|
||||
}
|
||||
check('I7 intrare cu state -> cookie sesiune obtinut', $cookie !== null);
|
||||
$r = http('GET', BASE . '/index.php?error=access_denied&error_description=Utilizatorul+a+refuzat',
|
||||
null, $cookie);
|
||||
check('I7 callback ?error= cu sesiune -> pagina eroare',
|
||||
$r['status'] === 200 && strpos($r['body'], 'Autorizarea nu a reușit') !== false,
|
||||
"status={$r['status']}");
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
$j = json_decode($r['body'], true);
|
||||
check('I7 pick.php livreaza eroarea ANAF',
|
||||
is_array($j) && isset($j['error']) && $j['error'] === 'access_denied',
|
||||
"body={$r['body']}");
|
||||
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
|
||||
check('I7 a doua cerere pick -> pending', $r['body'] === '{"status":"pending"}',
|
||||
"body={$r['body']}");
|
||||
|
||||
// ===================================================================
|
||||
// Rezumat
|
||||
// ===================================================================
|
||||
echo "\n$pass trecute, $fail esuate\n";
|
||||
exit($fail === 0 ? 0 : 1);
|
||||
1
oauth2/tokens/.htaccess
Normal file
1
oauth2/tokens/.htaccess
Normal file
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
Reference in New Issue
Block a user