Enhancements to DNS server and notes for usage with bind
This commit is contained in:
parent
b1c62ae227
commit
ccf01a1b23
@ -2,18 +2,39 @@
|
||||
|
||||
namespace App\Classes\Protocol;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Classes\Protocol as BaseProtocol;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
use App\Http\Controllers\DomainController;
|
||||
use App\Models\Address;
|
||||
use App\Models\{Address,Domain};
|
||||
|
||||
/**
|
||||
* Respond to DNS queries and provide addresses to FTN nodes.
|
||||
*
|
||||
* This implementation doesnt support EDNS nor DNSSEC.
|
||||
*
|
||||
* If using bind, the following configuration is required:
|
||||
* options {
|
||||
* validate-except
|
||||
* {
|
||||
* "ftn";
|
||||
* };
|
||||
* };
|
||||
*
|
||||
* and optionally
|
||||
* server <IP ADDRESS> {
|
||||
* edns no;
|
||||
* };
|
||||
*/
|
||||
final class DNS extends BaseProtocol
|
||||
{
|
||||
private const LOGKEY = 'PD-';
|
||||
|
||||
private const DEFAULT_TTL = 86400;
|
||||
private const TLD = 'ftn';
|
||||
|
||||
private BaseProtocol\DNS\Query $query;
|
||||
|
||||
// DNS Response codes
|
||||
@ -36,22 +57,8 @@ final class DNS extends BaseProtocol
|
||||
public const DNS_TYPE_TXT = 16; // TXT Records
|
||||
|
||||
public const DNS_TYPE_AAAA = 28; // AAAA Records
|
||||
|
||||
/**
|
||||
* Split a domain into a DNS domain string
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
private function domain_split(string $domain): string
|
||||
{
|
||||
$a = '';
|
||||
|
||||
foreach (explode('.',$domain) as $item)
|
||||
$a .= pack('C',strlen($item)).$item;
|
||||
|
||||
return $a;
|
||||
}
|
||||
public const DNS_TYPE_OPT = 41; // OPT Records
|
||||
public const DNS_TYPE_DS = 43; // DS Records (Delegation signer RFC 4034)
|
||||
|
||||
public function onConnect(SocketClient $client): ?int
|
||||
{
|
||||
@ -88,38 +95,6 @@ final class DNS extends BaseProtocol
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*
|
||||
* dig +noedns -t CNAME mail.dcml.au @1.1.1.1
|
||||
*
|
||||
* ; <<>> DiG 9.10.6 <<>> +noedns -t CNAME mail.dcml.au @1.1.1.1
|
||||
* ;; global options: +cmd
|
||||
* ;; Got answer:
|
||||
* ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6473
|
||||
* ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
||||
*
|
||||
* ;; QUESTION SECTION:
|
||||
* ;mail.dcml.au. IN CNAME
|
||||
*
|
||||
* ;; ANSWER SECTION:
|
||||
* mail.dcml.au. 300 IN CNAME l.dege.au.
|
||||
*
|
||||
* ;; Query time: 43 msec
|
||||
* ;; SERVER: 1.1.1.1#53(1.1.1.1)
|
||||
* ;; WHEN: Thu Apr 20 06:47:55 PST 2023
|
||||
* ;; MSG SIZE rcvd: 51
|
||||
* 06:47:54.995929 IP 192.168.130.184.51502 > one.one.one.one.domain: 6473+ CNAME? mail.dcml.au. (30)
|
||||
* 0x0000: cc03 d9cc 88cb bcd0 7414 d055 0800 4500 ........t..U..E.
|
||||
* 0x0010: 003a eee2 0000 4011 466e c0a8 82b8 0101 .:....@.Fn......
|
||||
* 0x0020: 0101 c92e 0035 0026 bb5f|1949 0120 0001 .....5.&._.I....
|
||||
* 0x0030: 0000 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml
|
||||
* 0x0040: 0261 7500 0005 0001 .au.....
|
||||
* 06:47:55.034171 IP one.one.one.one.domain > 192.168.130.184.51502: 6473$ 1/0/0 CNAME l.dege.au. (51)
|
||||
* 0x0000: bcd0 7414 d055 cc03 d9cc 88cb 0800 4588 ..t..U........E.
|
||||
* 0x0010: 004f 514a 4000 3a11 a969 0101 0101 c0a8 .OQJ@.:..i......
|
||||
* 0x0020: 82b8 0035 c92e 003b 9274|1949 81a0 0001 ...5...;.t.I....
|
||||
* 0x0030: 0001 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml
|
||||
* 0x0040: 0261 7500 0005 0001 c00c 0005 0001 0000 .au.............
|
||||
* 0x0050: 012c 0009 016c 0464 6567 65c0 16 .,...l.dege..
|
||||
*/
|
||||
public function protocol_session(): int
|
||||
{
|
||||
@ -127,15 +102,65 @@ final class DNS extends BaseProtocol
|
||||
|
||||
$this->query = new BaseProtocol\DNS\Query($this->client->read(0,512));
|
||||
|
||||
// If there is no query count, then its an error
|
||||
if ($this->query->qdcount !== 1) {
|
||||
Log::error(sprintf('%s:! DNS query doesnt have the right number of queries [%d]',self::LOGKEY,$this->query->qdcount));
|
||||
return $this->reply(self::DNS_FORMERR);
|
||||
Log::info(sprintf('%s:= DNS Query from [%s] for [%s]',self::LOGKEY,$this->client->address_remote,$this->query->domain));
|
||||
|
||||
// If the wrong class
|
||||
if ($this->query->class !== self::DNS_QUERY_IN) {
|
||||
Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
}
|
||||
|
||||
// We need a minimum of f.n.z.d.root
|
||||
if ($this->query->labels->count() < 5)
|
||||
return $this->nameerr($this->query->labels);
|
||||
$dos = Domain::select(['id','name','dnsdomain'])->active();
|
||||
$ourdomains = $dos
|
||||
->pluck('name')
|
||||
->transform(function($item) { return sprintf('%s.%s',$item,self::TLD); })
|
||||
->merge($dos->pluck('dnsdomain'))
|
||||
->merge([self::TLD])
|
||||
->filter()
|
||||
->sortBy(function($item) { return substr_count($item,'.'); })
|
||||
->reverse();
|
||||
|
||||
$query_domain = $this->query->domain;
|
||||
|
||||
// If the query is not for our domains, return NAMEERR
|
||||
if (($do=$ourdomains->search(function ($item) use ($query_domain) { return preg_match("/${item}$/",$query_domain); })) === FALSE) {
|
||||
Log::alert(sprintf('%s:= DNS Query not for our domains',self::LOGKEY));
|
||||
return $this->nameerr();
|
||||
}
|
||||
|
||||
// Action on the query type
|
||||
switch ($this->query->type) {
|
||||
// Return the SOA/NS records
|
||||
case self::DNS_TYPE_SOA:
|
||||
Log::info(sprintf('%s:= Returning SOA for [%s]',self::LOGKEY,$this->query->domain));
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize([
|
||||
$this->domain_split(gethostname()),
|
||||
$this->domain_split(Str::replace('@','.',config('app.mail.mail_from','nobody@'.gethostname()))),
|
||||
1,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL
|
||||
]) => self::DNS_TYPE_SOA],
|
||||
[],
|
||||
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS],
|
||||
);
|
||||
|
||||
case self::DNS_TYPE_NS:
|
||||
Log::info(sprintf('%s:= Returning NS for [%s]',self::LOGKEY,$this->query->domain));
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS]);
|
||||
|
||||
// Respond to A/AAAA/CNAME queries, with value or NAMEERR
|
||||
case self::DNS_TYPE_CNAME:
|
||||
case self::DNS_TYPE_A:
|
||||
case self::DNS_TYPE_AAAA:
|
||||
Log::info(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain));
|
||||
|
||||
$labels = clone($this->query->labels);
|
||||
|
||||
@ -145,53 +170,43 @@ final class DNS extends BaseProtocol
|
||||
$labels->shift();
|
||||
|
||||
if (is_null($f=$this->parse('f',$labels->shift())))
|
||||
return $this->nameerr($labels);
|
||||
return $this->nameerr();
|
||||
|
||||
if (is_null($n=$this->parse('n',$labels->shift())))
|
||||
return $this->nameerr($labels);
|
||||
return $this->nameerr();
|
||||
|
||||
if (is_null($z=$this->parse('z',$labels->shift())))
|
||||
return $this->nameerr($labels);
|
||||
return $this->nameerr();
|
||||
|
||||
if (is_null($d=$labels->shift()))
|
||||
return $this->nameerr($labels);
|
||||
return $this->nameerr();
|
||||
|
||||
// Make sure we have a root/base domain
|
||||
if (! $labels->count())
|
||||
return $this->nameerr($labels);
|
||||
return $this->nameerr();
|
||||
|
||||
$rootdn = $labels->join('.');
|
||||
|
||||
$ao = Address::findFTN(sprintf('%d:%d/%d.%d@%s',$z,$n,$f,$p,$d));
|
||||
|
||||
// Check we have the right record
|
||||
if ((! $ao) || (! $ao->system->mailer_address) || (($rootdn !== 'ftn') && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $d.'.'.$rootdn))))
|
||||
return $this->nameerr($labels);
|
||||
|
||||
// If the wrong class
|
||||
if ($this->query->class !== self::DNS_QUERY_IN) {
|
||||
Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
if ((! $ao) || (! $ao->system->mailer_address) || (($rootdn !== self::TLD) && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $d.'.'.$rootdn)))) {
|
||||
Log::alert(sprintf('%s:= No DNS record for [%d:%d/%d.%d@%s]',self::LOGKEY,$z,$n,$f,$p,$d));
|
||||
return $this->nameerr();
|
||||
}
|
||||
|
||||
// Check the class
|
||||
switch ($this->query->type) {
|
||||
case self::DNS_TYPE_CNAME:
|
||||
case self::DNS_TYPE_A:
|
||||
case self::DNS_TYPE_AAAA:
|
||||
Log::debug(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->mailer_address,$ao->ftn));
|
||||
$this->reply(
|
||||
self::DNS_NOERROR,
|
||||
$this->query->domain,
|
||||
[$this->domain_split($ao->system->mailer_address) => self::DNS_TYPE_CNAME]);
|
||||
break;
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($this->domain_split($ao->system->mailer_address)) => self::DNS_TYPE_CNAME]);
|
||||
|
||||
// Other attributes return NOTIMPL
|
||||
default:
|
||||
Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type));
|
||||
$this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
}
|
||||
|
||||
return self::DNS_NOERROR;
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -205,9 +220,9 @@ final class DNS extends BaseProtocol
|
||||
return pack('n',$offset | (3 << 14));
|
||||
}
|
||||
|
||||
private function nameerr(Collection $labels): int
|
||||
private function nameerr(): int
|
||||
{
|
||||
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$labels->join('.')));
|
||||
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain));
|
||||
|
||||
return $this->reply(self::DNS_NAMEERR);
|
||||
}
|
||||
@ -232,44 +247,53 @@ final class DNS extends BaseProtocol
|
||||
* Return a DNS response
|
||||
*
|
||||
* @param int $code
|
||||
* @param string $question
|
||||
* @param array $answer
|
||||
* @param array $authority
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function reply(int $code,string $question='',array $answer=[]): bool
|
||||
private function reply(int $code,array $answer=[],array $authority=[],array $additional=[]): bool
|
||||
{
|
||||
$header = (1 << 15); // 1b: Query/Response
|
||||
$header |= (0 << 11); // 4b: Opcode
|
||||
$header |= (0 << 10); // 1b: Authoritative Answer
|
||||
$header |= ($this->query->header & 0xf) << 11; // 4b: Opcode
|
||||
$header |= (1 << 10); // 1b: Authoritative Answer
|
||||
$header |= (0 << 9); // 1b: Truncated
|
||||
$header |= (0 << 8); // 1b: Recursion Desired (in queries)
|
||||
$header |= ((($this->query->header >> 8) & 1) << 8); // 1b: Recursion Desired (in queries)
|
||||
$header |= (0 << 7); // 1b: Recursion Available (in responses)
|
||||
$header |= (0 << 4); // 3b: Zero (future, should be zero)
|
||||
$header |= $code; // 4b: Result Code
|
||||
$header |= ($code & 0xf); // 4b: Result Code
|
||||
|
||||
$q = $question ? 1 : 0;
|
||||
$q = $this->query->dns ? 1 : 0;
|
||||
$r = count($answer);
|
||||
$nscount = 0;
|
||||
$arcount = 0;
|
||||
$nscount = count($authority);
|
||||
$arcount = count($additional);
|
||||
|
||||
$reply = pack('nnnnnn',$this->query->id,$header,$q,$r,$nscount,$arcount);
|
||||
|
||||
// Return the answer
|
||||
if ($r) {
|
||||
// Question
|
||||
$reply .= $question;
|
||||
$reply .= $this->query->dns;
|
||||
|
||||
// @todo In the case we return a CNAME and an A record, this should reference the CNAME domain when returning the A record
|
||||
foreach ($answer as $item => $type)
|
||||
$reply .= $this->rr($this->compress(12),$item,$type,300);
|
||||
|
||||
} else {
|
||||
$reply .= $question;
|
||||
foreach ($answer as $item => $type) {
|
||||
$rr = $this->rr($this->compress(12),unserialize($item),$type,self::DEFAULT_TTL);
|
||||
$reply .= $rr;
|
||||
}
|
||||
|
||||
// nscount
|
||||
//$reply .= $this->rr($this->domain_split('net1.fsxnet.nz'),["a","root-servers","net"],self::DNS_TYPE_NS,300);
|
||||
} else {
|
||||
$reply .= $this->query->dns;
|
||||
}
|
||||
|
||||
foreach ($authority as $item => $type) {
|
||||
$rr = $this->rr($this->compress(12),unserialize($item),$type,self::DEFAULT_TTL);
|
||||
$reply .= $rr;
|
||||
}
|
||||
|
||||
foreach ($additional as $item => $type) {
|
||||
$rr = $this->rr($this->compress(12),unserialize($item),$type,self::DEFAULT_TTL);
|
||||
$reply .= $rr;
|
||||
}
|
||||
|
||||
if (! $this->client->send($reply,0)) {
|
||||
Log::error(sprintf('%s:! Error [%s] sending DNS reply to [%s:%d]',
|
||||
@ -285,6 +309,24 @@ final class DNS extends BaseProtocol
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a domain into a DNS domain string
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
private function domain_split(string $domain): string
|
||||
{
|
||||
$a = '';
|
||||
|
||||
foreach (explode('.',$domain) as $item)
|
||||
$a .= pack('C',strlen($item)).$item;
|
||||
|
||||
$a .= "\x00";
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a DNS Resource Record
|
||||
*
|
||||
@ -294,7 +336,7 @@ final class DNS extends BaseProtocol
|
||||
* @param int $ttl - Time to live
|
||||
* @return string
|
||||
*/
|
||||
private function rr(string $query,string $ars,int $type,int $ttl): string
|
||||
private function rr(string $query,mixed $ars,int $type,int $ttl): string
|
||||
{
|
||||
// Reference the domain query in the question
|
||||
$reply = $query;
|
||||
@ -305,22 +347,26 @@ final class DNS extends BaseProtocol
|
||||
// Internet
|
||||
$reply .= pack('n',self::DNS_QUERY_IN);
|
||||
|
||||
$reply .= pack('n',0);
|
||||
|
||||
// TTL
|
||||
$reply .= pack('n',$ttl);
|
||||
$reply .= pack('N',$ttl);
|
||||
|
||||
// Answer
|
||||
$a = '';
|
||||
switch ($type) {
|
||||
case self::DNS_TYPE_CNAME:
|
||||
case self::DNS_TYPE_NS:
|
||||
$a = $ars."\x00";
|
||||
break;
|
||||
|
||||
case self::DNS_TYPE_CNAME:
|
||||
case self::DNS_TYPE_A:
|
||||
case self::DNS_TYPE_AAAA:
|
||||
$a = $ars;
|
||||
$a .= $ars;
|
||||
|
||||
break;
|
||||
|
||||
case self::DNS_TYPE_SOA:
|
||||
$a .= $ars[0];
|
||||
$a .= $ars[1];
|
||||
$a .= pack('NNNNN',$ars[2],$ars[3],$ars[4],$ars[5],$ars[6]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$reply .= pack('n',strlen($a)).$a;
|
||||
|
@ -3,15 +3,23 @@
|
||||
namespace App\Classes\Protocol\DNS;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
final class Query
|
||||
{
|
||||
private const LOGKEY = 'PDQ';
|
||||
|
||||
private string $buf;
|
||||
private int $class;
|
||||
private string $domain;
|
||||
private string $dns;
|
||||
private int $id;
|
||||
private int $type;
|
||||
|
||||
private int $arcount;
|
||||
private int $qdcount;
|
||||
|
||||
private RR $additional;
|
||||
|
||||
private Collection $labels;
|
||||
|
||||
// https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md
|
||||
@ -34,6 +42,8 @@ final class Query
|
||||
|
||||
$this->id = $header['id'];
|
||||
$this->qdcount = $header['qdcount'];
|
||||
$this->arcount = $header['arcount'];
|
||||
$this->header = $header['header'];
|
||||
|
||||
// Get the domain elements
|
||||
$this->labels = collect();
|
||||
@ -49,19 +59,40 @@ final class Query
|
||||
$this->type = $result['type'];
|
||||
$this->class = $result['class'];
|
||||
|
||||
$this->domain = substr($this->buf,$x=$this->header_len(),$rx_ptr-$x);
|
||||
$this->dns = substr($this->buf,$this->header_len(),$rx_ptr-$this->header_len());
|
||||
|
||||
// Do we have additional records
|
||||
if ($this->arcount) {
|
||||
// Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891
|
||||
if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) {
|
||||
Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY));
|
||||
// @todo catch this
|
||||
}
|
||||
|
||||
$this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack)));
|
||||
$rx_ptr += $this->additional->length;
|
||||
}
|
||||
|
||||
if (strlen($this->buf) !== $rx_ptr) {
|
||||
dd(['query remaining'=>strlen($this->buf)-$rx_ptr,'hex'=>hex_dump(substr($this->buf,$rx_ptr))]);
|
||||
}
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'class':
|
||||
case 'domain':
|
||||
case 'dns':
|
||||
case 'id':
|
||||
case 'labels':
|
||||
case 'qdcount':
|
||||
case 'arcount':
|
||||
case 'header':
|
||||
case 'type':
|
||||
return $this->{$key};
|
||||
|
||||
case 'domain':
|
||||
return $this->labels->join('.');
|
||||
}
|
||||
}
|
||||
|
||||
|
75
app/Classes/Protocol/DNS/RR.php
Normal file
75
app/Classes/Protocol/DNS/RR.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Protocol\DNS;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\Protocol\DNS;
|
||||
|
||||
final class RR
|
||||
{
|
||||
private const LOGKEY = 'PDR';
|
||||
|
||||
public Collection $labels;
|
||||
public int $type;
|
||||
public int $class;
|
||||
private string $buf;
|
||||
|
||||
public function __construct(string $buf)
|
||||
{
|
||||
$this->buf = $buf;
|
||||
$this->labels = collect();
|
||||
|
||||
$i = 0;
|
||||
|
||||
$domain = strstr($buf,"\x00",TRUE);
|
||||
$i += strlen($domain)+1;
|
||||
|
||||
$this->type = Arr::get(unpack('n',substr($buf,$i,2)),1);
|
||||
$this->class = Arr::get(unpack('n',substr($buf,$i+2,2)),1);
|
||||
$i += 4;
|
||||
|
||||
switch ($this->type) {
|
||||
case DNS::DNS_TYPE_CNAME:
|
||||
case DNS::DNS_TYPE_NS:
|
||||
case DNS::DNS_TYPE_DS:
|
||||
case DNS::DNS_TYPE_SOA:
|
||||
$i = 0;
|
||||
|
||||
while (($len=ord(substr($domain,$i++,1))) !== 0x00) {
|
||||
$this->labels->push(substr($buf,$i,$len));
|
||||
$i += $len;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case DNS::DNS_TYPE_OPT:
|
||||
// Domain is 0x00
|
||||
$this->ttl = Arr::get(unpack('N',substr($buf,$i,4)),1);
|
||||
$this->rddata_len = Arr::get(unpack('n',substr($buf,$i+4,2)),1);
|
||||
$this->rddata = substr($buf,$i+6,$this->rddata_len);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
dd(['unknown type:'.$this->type,'buf'=>$this->buf]);
|
||||
}
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'length':
|
||||
return strlen($this->buf);
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('%s:Unknown key [%s]',self::LOGKEY,$key));
|
||||
}
|
||||
}
|
||||
|
||||
public function __tostring(): string
|
||||
{
|
||||
return $this->buf;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user