Autor Zpráva
TomasJ
Profil
Zdravím všechny.
Již asi rok zpět jsem si udělal v PHP banovací systém pro web, využívající soubor (aby to fungovalo v každém případě). Banování, odbanování, vypršení banu funguje, jen mám problém s tím, že soubor se sám od sebe během jednoho dne vyprázdní. Zkoušel jsem všemožné věci, logování (jestli se náhodou nebere že ban vypršel), použití flock(), dokonce nejdřív vytvoření dočasného souboru a až pak přepis (smazání starého, přejmenování nového), ale nic nepomáhá a soubor je druhý den prázdný. A jak už to jistě znáte, když zjišťujete proč to chybuje, obvykle to jede naprosto skvěle a chyba nastane, až ji vůbec nečekáte.

Máte nějaké nápady, proč to tak je?

Přikládám jen třídu na banování, bez třídy na logy.

class Banlist{
  public  $someUBs;
  private $loaded,
          $banlist,
          $filepath,
          $ban_count,
          $bLog;
  
  public function __construct($fpath){
    if(empty($fpath)) throw new Exception("Path for banlist file is required!");
    $this->filepath = $fpath;
    $this->loaded = false;
    $logpath = substr($fpath,0,-4).".log";
    $this->bLog = new Log($logpath);
    $this->load();
  }

  public function load(){
    if(!file_exists($this->filepath)){
      @fclose(@fopen($this->filepath,"wb"));
      if(!file_exists($this->filepath)) throw new Exception("Banlist::load: Banlist file not exists!");
    }
    $this->banlist = Array();
    if($this->loaded === false){
      $this->loaded = true;
      $fsize = filesize($this->filepath);
      if($fsize){
        $fp = fopen($this->filepath, "rb");
        if($fp !== false){
          $bytes_readed = 0;
          while($bytes_readed < $fsize){
            //IP
            $ip = Array();
            $data = unpack("C",fread($fp,1)); //1. byte
            $ip[] = $data[1];
            $data = unpack("C",fread($fp,1)); //...
            $ip[] = $data[1];
            $data = unpack("C",fread($fp,1)); //...
            $ip[] = $data[1];
            $data = unpack("C",fread($fp,1)); //4. byte
            $ip[] = $data[1];
            $ip = implode(".",$ip);
            
            //Banned - timestamp
            $bantime = 0;
            $data = unpack("l",fread($fp,4));
            $bantime = $data[1];
            
            //Ban length
            $banlen = 0;
            $data = unpack("l",fread($fp,4));
            $bytes_readed += 12;
            $banlen = $data[1];
            $this->banlist[] = Array("ip"=>$ip,"banTime"=>$bantime,"banLen"=>$banlen);
          }
          fclose($fp);
        }
        else throw new Exception("Banlist::load: Can't open the banlist file for reading.");
      }
      $this->ban_count = count($this->banlist);
    }
  }
  
  public function ban($ip,$len=-1){
    if($this->loaded === false) throw new Exception("Banlist::ban: Banlist is not loaded!");
    if($len>=-1){
      $this->bLog->write("IP $ip was banned from {$_SERVER['REMOTE_ADDR']}. Length: ".($len == -1 ? "Permanently" : "$len seconds"));
      array_push($this->banlist,Array("ip"=>$ip,"banTime"=>time(),"banLen"=>$len));
      $this->ban_count++;
      return true;
    }
    return false;
  }

  public function unban($ip=false,$expired=false){
    if($this->loaded === false) throw new Exception("Banlist::unban: Banlist is not loaded!");
    if($ip !== false){
      $banlist = Array();
      foreach($this->banlist as $row){
        $found = array_search($ip,$row);
        if($found === false) array_push($banlist,$row);
        else{
          $this->ban_count--;
          $this->someUBs++;
          if($row['banLen'] != -1 && $row['banTime']+$row['banLen'] < time()) $expired = true;
          if(!$expired) $this->bLog->write("IP $ip was unbanned.");
          else $this->bLog->write("IP $ip was unbanned due to expiration of the ban length (expired ".Date("d.m.Y H:i:s",$row['banTime']+$row['banLen']).").");
        }
      }
      $this->banlist = $banlist;
    }
  }

  public function get(){
    if($this->loaded === false) throw new Exception("Banlist::get: Banlist is not loaded!");
    return $this->banlist;
  }

  public function isBannedIP($ip=false){
    if($this->loaded === false) throw new Exception("Banlist::isBannedIP: Banlist is not loaded!");
    if($ip !== false){
      foreach($this->banlist as $row){
        $found = array_search($ip,$row);
        if($found !== false){
          if($row['banLen']>=-1){
            if($row['banLen'] == -1) return $row;
            if(time() > $row['banTime']+$row['banLen']) $this->unban($ip,true);
            else return $row;
          }
        }
      }
    }
    return false;
  }

  public function save(){
    if($this->loaded === false) throw new Exception("Banlist::save: Banlist not loaded!");
    $fp = fopen($this->filepath."_tmp", "wb");
    if($fp !== false){
      if(@flock($fp, LOCK_EX)){
        foreach($this->banlist as $row){
          if(!empty($row) && is_array($row)){
            $partIp = explode(".",$row['ip'],4);
            $ip = pack("c",$partIp[0]).pack("c",$partIp[1]).pack("c",$partIp[2]).pack("c",$partIp[3]);
            fwrite($fp,$ip.pack("L",$row['banTime']).pack("L",$row['banLen']));
          }
        }
        flock($fp, LOCK_UN);
      }
      fclose($fp);
      if(@unlink($this->filepath) !== false){
        if(@rename($this->filepath."_tmp", $this->filepath) === false) throw new Exception("Banlist::save: Can't rewrite the banlist file (rename).");
        chmod($this->filepath, 0775);
      }
      else throw new Exception("Banlist::save: Can't rewrite the banlist file (delete).");
    }
    else throw new Exception("Banlist::save: Can't create a temporary banlist file.");
  }

  public function quantity(){
    return $this->ban_count;
  }

  public function __destruct(){
    unset($this->loaded);
    unset($this->banlist);
    unset($this->filepath);
    unset($this->ban_count);
  }
}

Při každém načtení stránky zavolám $instance->isBannedIP($_SERVER['REMOTE_ADDR']) a pak ještě zjistím, jestli bylo provedeno nějaké automatické odbanování if($instance->someUBs > 0) a pokud ano tak $instance->save()

A ano, jsem si vědom, že některé věci dělám navíc, jenže jsem třídu psal před rokem, kdy jsem nemyslel tolik na náročnost vykonávání (mluvím momentálně třeba o přepisu celé proměnné v metodě unban).
Jan Tvrdík
Profil
TomasJ:
Ideálně zahodit a napsat znovu, třeba s použitím SQLite databáze. Nebo si budeš muset nastudovat, jak atomicky pracovat se soubory, což je na mozek výrazně náročnější. Konkrétně tady máš neatomické řádky 131 a 132 (kvůli tomu se ti to zřejmě promazává). Plus je to neatomické jako celek, protože si nedržíš zámek exkluzivní zámek nad tím původním souborem, takže si to můžeš přepsat starými daty.
TomasJ
Profil
Jan Tvrdík:
Konkrétně tady máš neatomické řádky 131 a 132 (kvůli tomu se ti to zřejmě promazává).
Tak to není, to jsem ze zoufalství přidal, protože se promazávalo i předtím. Dřív jsem ani flock neměl a promazávalo se to.

třeba s použitím SQLite databáze
Jede SQLite bez instalace? Protože to chci využít vícekrát a všude nemám právo instalovat balíčky.
Jan Tvrdík
Profil
TomasJ:
Dřív jsem ani flock neměl a promazávalo se to.
Zřejmě si jednu chybu nahradil za jinou =)

Jede SQLite bez instalace?
Bez instalace nefunguje ani PHP, ale jinak SQLite by mělo být standardně dostupné od PHP 5.3.0, viz cz1.php.net/manual/en/sqlite3.installation.php.
TomasJ
Profil
Jan Tvrdík:
Nejdřív vyzkouším tu atomickou práci se soubory, a když se nezadaří, Přejdu na sqlite.
Díky.
TomasJ
Profil
Tak jsem studoval narychlo tu atomickou práci se soubory a slátal jsem něco dohromady:
  public function save(){
    if($this->loaded === false) throw new Exception("Banlist::save: Banlist not loaded!");
    $fp = fopen($this->filepath, "r+");
    if($fp !== false){
      $locked = false;
      while(!$locked){
        if(@flock($fp,LOCK_EX)) $locked = true;
        else usleep(10000);
      }
      fseek($fp,0,SEEK_END);
      $flen = ftell($fp);
      rewind($fp);
      ftruncate($fp,$flen);
      foreach($this->banlist as $row){
        if(!empty($row) && is_array($row)){
          $partIp = explode(".",$row['ip'],4);
          $ip = pack("c",$partIp[0]).pack("c",$partIp[1]).pack("c",$partIp[2]).pack("c",$partIp[3]);
          fwrite($fp,$ip.pack("L",$row['banTime']).pack("l",$row['banLen']));
        }
      }
      flock($fp, LOCK_UN);
      fclose($fp);
    }
    else throw new Exception("Banlist::save: Can't open the file for writing.");
  }
Uživatel si počká o několik desítek milisekund déle, ale myslím, že to není tak strašné, co říkáte? Dál by šlo taky vytvořit dočasný soubor, kterému bych kontroloval existenci, že?
Jan Tvrdík
Profil
TomasJ:
Pořád to není dobře:
1) $this->banlist sestavíš předtím, než získáš zámek, takže v době zápisu už nemusí být aktuální.
2) flock na řádku 7 nemusí teoreticky nikdy uspět. Lépe je zkusit to třeba 10× a pak se na to vykašlat.
3) Musíš použít zámek i při čtení, jinak můžeš přečíst „rozepsaný soubor“.
4) Zámek jako takový umí garantovat izolaci, ale nikoliv atomicitu. Tedy selže-li proces provádějící zápis, tak o data přijdeš. Řeší se to obvykle zápisem do dočasného souboru a následným přejmenováním.

…nebo použiješ databázi, která tohle všechno řeší, že =)
Kajman
Profil
TomasJ:

Pokud se na server např. každý den dávají soubory z vývojového serveru nebo repozitáře, zkontrolujte, zda máte soubor s banovacími informacemi ve výjimkách. To platí i pro případný sqlite soubor.
TomasJ
Profil
Jan Tvrdík:
3) Musíš použít zámek i při čtení, jinak můžeš přečíst ‚rozepsaný soubor‘.
Ano, pro čtení mám LOCK_SH

oužiješ databázi, která tohle všechno řeší, že =)
Mně spíš jde o to, rozvíjet své znalosti a nevykakat se na to, protože to je složité na přemýšlení :)

Kajman:
Pokud se na server např. každý den dávají soubory z vývojového serveru nebo repozitáře, zkontrolujte, zda máte soubor s banovacími informacemi ve výjimkách. To platí i pro případný sqlite soubor.
Nene, tohle se neděje

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: