WordPress kullanıyorsanız bu sayfaya ihtiyacınız yok: glimana Publisher eklentisini kurup glimana’daki bağlantı kodunu yapıştırmanız yeterli. Bu belge, kendi yazılımını kullanan siteler içindir: aşağıdaki iki uç noktayı siz uygularsınız, glimana onlara imzalı istek gönderir.
Akış
- glimana’da Ayarlar → Yayın bağlantıları → Özel API ile bağlantı oluşturun; uç nokta tabanını girin (ör.
https://ornek.com/api/glimana). glimana size bir kez bir anahtar kimliği ve gizli anahtar gösterir. - Sunucunuzda iki uç nokta açın:
GET {taban}/statusvePOST {taban}/posts. - Her istekte imzayı aşağıdaki gibi doğrulayın; doğrulanamayan isteği hiçbir işlem yapmadan 401 ile reddedin.
- glimana’da “Bağlantıyı test et”e basın.
İstek başlıkları
| Başlık | Değer |
|---|---|
X-Glimana-Key | Anahtar kimliği (16 onaltılık karakter) |
X-Glimana-Timestamp | Unix zamanı (saniye, UTC) |
X-Glimana-Nonce | 32 onaltılık karakter, her istekte tek |
X-Glimana-Action | status, posts.create, posts.get:{id} ya da posts.update:{id} |
X-Glimana-Signature | v1= + 64 onaltılık karakter |
İmza
Kanonik metin altı satırdır, satır sonu tek \n; gövde, isteğin ham baytlarıdır (JSON’u ayrıştırıp yeniden üretmeyin):
v1
{X-Glimana-Timestamp}
{X-Glimana-Nonce}
{HTTP YÖNTEMİ, büyük harf}
{X-Glimana-Action}
{sha256_hex(ham gövde)} ← GET için boş gövdenin özeti
imza = "v1=" + hmac_sha256_hex(gizli_anahtar, kanonik_metin)
Adres değil eylem imzalanır; böylece ters vekil sunucu ya da yönlendirme adresi değiştirse de imza bozulmaz. Bu yüzden eylemi uç noktanızla eşleştirmek sizin işiniz: /status yalnız status; /posts POST'ta posts.create, GET /posts?id= için posts.get:{id}, güncellemede posts.update:{id} kabul etmeli (id = sizin yazı kimliğiniz). glimana arşiv sayfasını açınca ve yeniden göndermeden önce posts.get sorar; silinmiş yazı için 404 + {"error":"glimana_not_found"} döndürün — yoksa bu istekler "hatalı imza" sayılıp sunucunuz glimana'yı kilitleyebilir.
Zorunlu güvenlik kuralları
- Sabit zamanlı karşılaştırma kullanın (PHP
hash_equals, Nodecrypto.timingSafeEqual, Pythonhmac.compare_digest).==zamanlama saldırısına açıktır. - Zaman penceresi: zaman damgası sunucu saatinizden 300 saniyeden fazla saparsa reddedin. Sunucu saatinizi NTP ile eşitleyin.
- Tekrar saldırısı: gördüğünüz nonce’u en az 10 dakika saklayın; aynı nonce ikinci kez gelirse reddedin.
- Yalnız HTTPS. glimana http:// adrese ve özel/iç ağ IP’lerine istek göndermez.
- Gövde sınırı koyun (öneri 2 MB) ve imzayı JSON’u ayrıştırmadan ÖNCE doğrulayın.
- İçeriğe güvenmeyin:
content_htmlglimana’da temizlenmiş olsa da kendi tarafınızda izin listesiyle (ör. HTMLPurifier, DOMPurify, bleach) yeniden süzün;script,iframe,on*öznitelikleri,javascript:bağlantıları geçmesin. - Yazar ve yetki sizde: yazıyı sizin belirlediğiniz bir kullanıcı adına oluşturun; istekten yazar, rol ya da ayar kabul etmeyin.
- Hemen yayın yalnız sizin açtığınız bir ayarla mümkün olsun; varsayılan taslak.
statusyanıtındaallow_publishile bildirin. - Hız sınırı: öneri saatte en çok 30 yazı; ardışık hatalı imzada IP’yi geçici kilitleyin.
- Gizli anahtarı yalnız sunucuda, kod deposunun ve kütüklerin dışında saklayın. Sızdığından şüphelenirseniz glimana’da “Anahtarı yenile” deyin; eskisi anında geçersiz olur.
- İsterseniz yalnız glimana’nın çıkış IP’sine izin verin:
209.74.74.26
GET {taban}/status
Bağlantı testinde çağrılır. Gövde boştur. Yanıt:
200 OK
{
"ok": true,
"site_name": "Örnek Site",
"plugin_version": "my-cms-1.0",
"allow_publish": false,
"categories": [ { "id": 3, "name": "Rehberler" } ],
"authors": [ { "id": 2, "name": "Editör" } ], ← glimana'nın makale bazlı seçebileceği yazarlar (sizin izin listeniz)
"default_author": 2
}
POST {taban}/posts
{
"idempotency_key": "0b6f6c0e-3c1a-4c55-9a51-2f1d5b7d9e10",
"title": "Kira artış oranı nasıl hesaplanır",
"content_html": "<h2>…</h2><p>…</p>",
"excerpt": "140-155 karakterlik özet",
"meta_description": "140-155 karakterlik özet",
"slug": "kira-artis-orani-nasil-hesaplanir",
"status": "draft", ← draft | future | publish
"date_gmt": "2026-10-08T07:30:00Z", ← yalnız status=future; her zaman UTC
"categories": [3],
"author_id": 2, ← isteğe bağlı; izin listenizde yoksa varsayılan yazarı kullanın
"language": "tr"
}
- idempotency_key: aynı anahtarla ikinci istek gelirse yeni yazı OLUŞTURMAYIN, ilkini döndürün. glimana ağ hatasında yeniden dener.
- future:
date_gmten az 1 dakika ileride olmalı; geçmiş tarih gelirse yayınlamayın, 422 dönün (aksi hâlde yazı yanlışlıkla hemen yayına çıkar). - publish: siz izin vermediyseniz 403 dönün.
- faq: 7 Ekim 2026'dan beri gönderilmiyor. Google SSS (FAQ) zengin sonucunu Mayıs 2026'da kaldırdı;
FAQPageyapısal verisi basmanın artık arama karşılığı yok. Soru-cevaplarcontent_htmliçinde görünür hâlde gelmeye devam ediyor. Alanı okuyan eski entegrasyonlar onu yok saymalı; sitenizde zaten basılmış FAQPage işaretlemesi zarar vermez, silmeniz gerekmez. - images (isteğe bağlı, en çok 3):
[{"key": "img0", "featured": true, "alt": "…", "mime": "image/jpeg", "data_base64": "…"}].featuredolan kapak (öne çıkan) görseldir; diğerleri gövdede<p>[[glimana-image:img1]]</p>jetonunun yerine konur. Görselleri desteklemiyorsanız jetonları silin. Güvenlik:mimeve uzantıya güvenmeyin; türü baytlardan tespit edin, görseli yeniden kodlayın (meta veri ve dosyaya eklenmiş içerik düşer), dosya adını kendiniz verin, tek görseli 1,5 MB ile sınırlayın.
Başarılı yanıt (yeni yazı 201, tekrar 200):
{ "id": 812, "status": "draft", "url": "https://ornek.com/?p=812", "edit_url": "https://ornek.com/admin/posts/812" }
Hata yanıtı: uygun durum kodu (400, 401, 403, 413, 422, 429, 500) ve {"code": "...", "message": "..."}. message kullanıcıya gösterilir; gizli bilgi koymayın. 401 ve 403’te glimana yeniden denemez; 429 ve 5xx’te iki kez daha dener.
Doğrulama örnekleri
PHP
function glimana_dogrula(string $gizli, array $h, string $yontem, string $beklenenEylem, string $govde): bool {
$ts = (int) ($h['x-glimana-timestamp'] ?? 0);
$nonce = (string) ($h['x-glimana-nonce'] ?? '');
if (($h['x-glimana-action'] ?? '') !== $beklenenEylem) return false;
if (abs(time() - $ts) > 300 || !preg_match('/^[a-f0-9]{32}$/', $nonce)) return false;
$kanonik = implode("\n", ['v1', (string) $ts, $nonce, strtoupper($yontem), $beklenenEylem, hash('sha256', $govde)]);
$beklenen = 'v1=' . hash_hmac('sha256', $kanonik, $gizli);
if (!hash_equals($beklenen, (string) ($h['x-glimana-signature'] ?? ''))) return false;
return nonce_ilk_kez_mi($nonce, 600); // kendi deponuz: Redis SET NX EX 600 / veritabanı benzersiz satır
}
// $govde = file_get_contents('php://input'); ← ham gövde
Node.js
const crypto = require('crypto');
function dogrula(gizli, h, yontem, beklenenEylem, hamGovde /* Buffer */) {
const ts = Number(h['x-glimana-timestamp']), nonce = String(h['x-glimana-nonce'] || '');
if (h['x-glimana-action'] !== beklenenEylem) return false;
if (Math.abs(Date.now() / 1000 - ts) > 300 || !/^[a-f0-9]{32}$/.test(nonce)) return false;
const kanonik = ['v1', String(ts), nonce, yontem.toUpperCase(), beklenenEylem,
crypto.createHash('sha256').update(hamGovde).digest('hex')].join('\n');
const beklenen = Buffer.from('v1=' + crypto.createHmac('sha256', gizli).update(kanonik).digest('hex'));
const gelen = Buffer.from(String(h['x-glimana-signature'] || ''));
return gelen.length === beklenen.length && crypto.timingSafeEqual(gelen, beklenen) && nonceIlkKezMi(nonce);
}
// Express: app.post('/api/glimana/posts', express.raw({ type: 'application/json', limit: '2mb' }), ...)
Python
import hashlib, hmac, time, re
def dogrula(gizli: bytes, h: dict, yontem: str, beklenen_eylem: str, ham_govde: bytes) -> bool:
ts, nonce = int(h.get('x-glimana-timestamp', 0)), h.get('x-glimana-nonce', '')
if h.get('x-glimana-action') != beklenen_eylem: return False
if abs(time.time() - ts) > 300 or not re.fullmatch(r'[a-f0-9]{32}', nonce): return False
kanonik = '\n'.join(['v1', str(ts), nonce, yontem.upper(), beklenen_eylem, hashlib.sha256(ham_govde).hexdigest()])
beklenen = 'v1=' + hmac.new(gizli, kanonik.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(beklenen, h.get('x-glimana-signature', '')) and nonce_ilk_kez_mi(nonce)
Test vektörü
Uygulamanızı bununla sınayın (gerçek anahtar DEĞİLDİR):
gizli : test_secret_do_not_use_0123456789abcdefABCDE
zaman : 1767225600
nonce : 4f6b1c2d3e4f5a6b7c8d9e0f1a2b3c4d
yöntem : POST
eylem : posts.create
gövde : {"idempotency_key":"0b6f6c0e-3c1a-4c55-9a51-2f1d5b7d9e10","title":"Hello","content_html":"<p>Hi</p>","status":"draft"}
gövde sha : 1d30b224b239dc0093e24dcbdd7ce6fa0ddc12f8ad49892e1cce4ebf335ed72a
imza : v1=ba6481f4574fb36b8e00ae74408bd54f81afc38fe446d8ab2c96ca0099af8abd
GET status (boş gövde, aynı zaman/nonce):
imza : v1=ac5b10064ac28b51e173db45934e55781ea817b6d6c205a6f57faf30daa74eb8