EN /HU | Belépés

Adat és hálózat

Amit a tartalmas boardjaink korábban mind maguknak írtak meg, az a 0.33.0 és az 1.0.0 óta a rubbs része: egységes naplóformátum, háttérmunka, SQLite-fájl migrációkkal, korlátok közé szorított HTTP, cache, rate limit, ütemezett Wikipédia-kliens, vezérlőkaraktermentes szöveg, és a szabály, hogy hol kezeljük egy hiba következményeit. Mindegyik elem néhány sorral bevethető; a teljes szignatúrák az API-referenciában vannak.

A config-mezők

A BBS.configure alábbi mezői tartoznak ide (a többi az Első lépések és a Telnet és a store oldalon van):

Mező Alapértelmezés Jelentés
log_tag ENV['BBS_LOG_TAG'] vagy bbs Amivel minden naplósor kezdődik: a board technikai neve, pl. feedia-bbs.
user_agent rubbs/VERSION (+https://teletypegames.org) A BBS::HTTP kérések User-Agent-je.
max_sessions_per_ip ENV['BBS_MAX_SESSIONS_PER_IP'] vagy 6 Egy nyilvános címről egyszerre futó hívások; a következőnek szól, és bont (a naplóban BBS crowded).
ai_concurrency 2 A folyamatban egyszerre futó BBS::AI::Client kérések; a következő legfeljebb 5 s-ot vár, aztán BBS::AI::Busy-t kap (lásd AI-karakterek).
RUBY
BBS.configure do |c|
  c.log_tag    = 'feedia-bbs'
  c.user_agent = 'Feedia BBS (+https://teletypegames.org)'
end

Naplózás és háttérmunka

Naplózás

Minden sor, amit a könyvtár vagy a board a naplóba ír, a BBS.log(tag, message)-en át megy a stderr-re, így tizenegy board sorai egy naplóban is egyformán olvashatók. Ha az üzenet kivétel, az osztálya és az üzenete kerül a sorba.

RUBY
BBS.log('catalog refresh', 'done in 3.2 s')   # feedia-bbs: catalog refresh: done in 3.2 s
BBS.log('catalog refresh', error)             # feedia-bbs: catalog refresh: SocketError: …

A könyvtár saját sorai is ezt a formát követik: session, door …, gateway …, store, BBS busy, BBS crowded. Egy BBS::DiscordWebhook label:-je is a log tag lett, úgyhogy már nem kell bele a board neve.

Háttérmunka

Ami a hívók útjából kifér (az induláskor betöltött index, az óránként frissülő katalógus), az egy BBS::Background szálban fut. A hibát naplózza, és retry_after (5) másodperc múlva újrapróbálja, a szünetet max_retry-ig (300) duplázva: egy leállt szolgáltatás így pár naplósorba kerül, nem egy összeomlott boardba vagy egy pörgő ciklusba.

RUBY
BBS.background('index') { wiki.load_index }                         # until it succeeds once
BBS.background('catalog refresh', every: 3600) { library.refresh }  # every hour

Mindkettő a Thread-et adja vissza; a BBS::Background.new(name, …) { … }.run ugyanezt a hívó szálban futtatja (egy tesztben, egy parancssori eszközben).

Tárolás

BBS::SQLite

Egy SQLite-fájl és az egyetlen kapcsolata, amelyen a hívók szálai egy zár mögött osztoznak: WAL mód (a board egy parancssori eszköze közben is használhatja a fájlt), busy timeout, bekapcsolt idegen kulcsok. A séma migrációk listája, a PRAGMA user_version számozza őket: egy fájl csak azokat futtatja, amelyeken még nem ment át, mindegyiket egy tranzakcióban. A migráció SQL-string vagy lambda; a már lefutottat sose írd át, inkább fűzz hozzá újat.

RUBY
DB = BBS::SQLite.new('data/board.sqlite3')        # creates data/ when needed
DB.migrate(
  'CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL)',
  'ALTER TABLE posts ADD COLUMN body TEXT',
  ->(db) { db.insert('INSERT INTO posts (title) VALUES (?)', 'Welcome') }
)
DB.query('SELECT * FROM posts')                    # => [{ 'id' => 1, 'title' => 'Welcome', 'body' => nil }]
DB.get('SELECT * FROM posts WHERE id = ?', 1)      # the first row or nil
DB.insert('INSERT INTO posts (title) VALUES (?)', 'Hi')   # => the new rowid
DB.change('DELETE FROM posts WHERE id = ?', 2)     # => rows changed
DB.transaction { … }                               # a nested call joins the outer one

BBS::Store

A Flow persist lépésének egytáblás, kulcs szerint upsertelő store-ja a 0.33.0 óta BBS::SQLite-ra épül. A .csv útvonal a mellette lévő .sqlite3 fájlt jelenti, egy régebbi board CSV-jét pedig az első használatkor beimportálja (a hibás sorokat egy .rejected fájlba teszi félre); a store.open! ezt induláskor elvégzi. Lásd Telnet és a store.

Hálózat

BBS::HTTP

Korlátok közé szorított kérések a szolgáltatások felé, amelyeket a board megmutat. Minden hiba BBS::HTTP::Error-t dob, amelynek az üzenete a címet a query string nélkül nevezi meg (így token nem kerül a naplóba); a max_bytes-nál nagyobb törzs az alosztályát, a BBS::HTTP::TooLarge-ot dobja. A nem 2xx státuszokat visszaadja, nem dob rájuk.

RUBY
response = BBS::HTTP.get('https://example.org/feed.xml', headers: { 'Accept' => 'application/rss+xml' })
response.ok?       # => true for 2xx
response.status    # => 200
response.body      # => binary String

BBS::HTTP.post(url, body: URI.encode_www_form(query: q),
               headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })

File.open(path, 'wb') { |file| BBS::HTTP.get(url, to: file, max_bytes: 64 * 1024 * 1024) }
Opció Alapértelmezés
open_timeout / read_timeout 5 / 15 s kapcsolatonként / olvasásonként
deadline 30 s az egész kérés, az átirányításokkal együtt
max_bytes 4 MiB a törzs folyamosan jön, és e fölött a kérés elbukik
redirects 3 (post: 0) csak http/https címre követi
public_only true elutasítja a loopback, privát, link-local, CGNAT, ULA és más nem nyilvános címre feloldódó hostokat; az ellenőrzött címre kapcsolódik
to nil egy IO, amelybe a 2xx törzs érkezés közben íródik (a body ilyenkor nil)

BBS::Cache

TTL, a legrégebben használt elem kidobása, bájtkorlát, kulcsonként egyetlen számítás (az ugyanazt a kulcsot egyszerre hiányoló hívók megvárják, és osztoznak az eredményén), és ha a számítás hibát dob, a lejárt értéket adja.

RUBY
ARTICLES = BBS::Cache.new(ttl: 600, max_entries: 500, max_bytes: 8 * 1024 * 1024)
ARTICLES.fetch([lang, title]) { repository.article(lang, title) }

BBS.background('catalog', every: 3600) { CATALOG.refresh(:all) { repository.load } }

A refresh(key) { … } csak akkor cseréli le a tárolt értéket, ha a blokk sikerrel fut, így egy háttérfrissítés sosem üríti ki a cache-t. Továbbá: delete(key), clear, size.

BBS::RateLimit

Kulcsonkénti csúszó ablak, thread-safe; a csendes kulcsokat elfelejti. A caller_key(session) interneten át a hívó IP-címe, egy helyi proxy (bbs-web) mögött pedig a vonal session-azonosítója, mert ott mindenki ugyanarról a címről jön.

RUBY
TIPS = BBS::RateLimit.new(limit: 3, per: 3600)
key  = BBS::RateLimit.caller_key(session)
TIPS.allow?(key)   # => true, and counted; false when over the limit
TIPS.wait(key)     # => seconds until the next event is allowed

PER_CALLER = BBS::RateLimit.new(limit: 5, per: 600)
PER_BOARD  = BBS::RateLimit.new(limit: 100, per: 600)
BBS::RateLimit.allow_all?([[PER_CALLER, key], [PER_BOARD, :all]])   # counted in all or none

LOGINS = BBS::RateLimit.new(limit: 10, per: 600, store: DB, name: 'logins')
return locked_out unless LOGINS.room?(key)
LOGINS.record(key) unless password_ok?(name, password)
  • Az allow_all? egyszerre több korlátot néz (a hívóét és az egész boardét): az eseményt vagy mindegyikbe beszámolja, vagy ha valamelyikben nincs hely, egyikbe sem.
  • A room? csak ellenőriz, a record csak számol: arra az esetre, amikor egy esemény csak akkor számít, ha rosszul sült el (rossz jelszó).
  • store:-ral (egy BBS::SQLite) az események a rate_limit_events táblájába kerülnek name: alatt, így egy belépési tiltás az újraindítást is túléli.

BBS::Wikipedia

A Wikipédia Action API-ja úgy, ahogy egy board kérdezi: BBS::HTTP-n át, a board User-Agentjével (anélkül a Wikipédia 403-at adhat), ütemezve, hogy a hívók együtt se küldjenek másodpercenként rate-nél több kérést; amelyik kérés 3 s-nál tovább várna a sorára, el sem megy. A válasz feldolgozva jön (a format=json és a formatversion=2 magától hozzáadódik); a hiba BBS::Wikipedia::Error, ami egy BBS::HTTP::Error. Az Alexandria BBS és a Commodex BBS használja.

RUBY
WIKIPEDIA = BBS::Wikipedia.new(user_agent: 'Alexandria BBS (+https://teletypegames.org)', rate: 5)
WIKIPEDIA.get('en', action: 'query', list: 'search', srsearch: 'Commodore 64')
# => { "query" => { "search" => [...] } }

A gateway védelme és fordítása

A BBS::Gateway (lásd Doorok) egyszer oldja fel a hostot, és az 1.0.0 óta csak nyilvános címet hív: egy listázás, amely 127.0.0.1-et vagy egy privát címet ad meg, nem teheti a boardot a saját hálózatába vezető átjáróvá. Az a board, amely a saját szomszédait tárcsázza, ezt ki is mondja; egy C64 pedig translate: true-val ASCII- és ANSI-terminálokra készült boardot is felhívhat (0.34.0 óta).

RUBY
BBS::Gateway.new(host: 'host.docker.internal', port: 2327, name: 'Feedia', public_only: false)
BBS::Gateway.new(host: board.host, port: board.port, translate: !board.petscii?).call(session)

Szöveg és címek

  • BBS::Text.clean(text): érvényes UTF-8, C0 vezérlőkarakterek (a tab és a soremelés marad), DEL, C1 és kétirányú felülíró karakterek nélkül. keep_newlines: false-szal minden sorvég szóköz lesz, így két sor szavai nem ragadnak össze.
  • BBS::Text.line(text): egyetlen tiszta sor, a whitespace-sorozatok (a sorvégek és a tabok is) egy szóközzé válnak, a két végén nincs szóköz. Címhez, névhez, összefoglalóhoz.
  • BBS::Text.printable?(char): minden beviteli mező szabálya.
  • BBS::Address.public?(ip) (a nyilvános interneten van) és BBS::Address.local?(ip) (privát, loopback, link-local).

A frame buffer sem tárol vezérlőkaraktert, így egy szolgáltatástól jött szöveg nem mozgathatja és nem színezheti át a hívó képernyőjét.

Hol kezeljük a hibát

Egy board rétegei egy szolgáltatás hibáját egyetlen helyen kezelik:

  • a repository (lib/domain/repository) beszél a külvilággal, és a hibáit továbbengedi: a BBS::HTTP::Error-t (és a BBS::Wikipedia::Error-t) úgy, ahogy jön, vagy a board egy domain-hibáját, ha magával a válasszal van baj. Nem alakítja őket nil-lé vagy üres listává, és nem ír szöveget a hívónak;
  • a service dönti el, mit lát a hívó: elkapja ezeket a hibákat, ír egy BBS.log sort, és azt adja vissza, amit a UI megmutat (egy cache-elt választ, egy üres eredményt, egy saját üzenetet);
  • a UI sosem kap el BBS::HTTP::Error-t; azt mutatja, amit a service adott.

Így URL-t vagy tokent tartalmazó hibaüzenet sosem jut el a hívóig, és minden hiba egyszer kerül a naplóba.

RUBY
class Library
  def search(query)
    repository.search(query)
  rescue BBS::HTTP::Error => e
    BBS.log('search', e)
    []
  end
end

Lásd még