Skip to content

Commit

Permalink
[需求] 完成基本功能
Browse files Browse the repository at this point in the history
  • Loading branch information
kamlyli committed Dec 1, 2019
0 parents commit 23de647
Show file tree
Hide file tree
Showing 12 changed files with 665 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

.idea
vendor
composer.lock
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

### 2019-10-28 v0.6

修复 receive 没有限制次数导致无限循环 (感谢 杨昕 发现的 bug )

### 2019-7-29 v0.5

改变类名(Kamly/DomainParser),优化使用文档

### 2019-7-23 v0.4

增加异常类(Exception),测试代码

### 2019-5-18 v0.3

调整代码架构(拆分类,Tools,DomainTcpParserException)

### 2019-5-18 v0.2

调整代码架构(拆分类,TcpSocket,DomainTcpParser,Manager)

### 2019-5-14 v0.1

完成基础版本


97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@

## Dns

目的:通过 socket 请求 指定 DNS 服务器,获取需要解析域名的 IP 地址

解决痛点:php 解决 gethostbyname 没法指定 dns 服务器地址

## 目前

处理 DNS协议的类型 1 和 5

待续处理 ipv6 ?

## 安装

```sh
$ composer require kamly/domain-parser -vvv
```

## 使用

```php
<?php

require_once './vendor/autoload.php';

use Kamly\DomainParser\Manager;

// 正确用法 指定域名
var_dump((new Manager())->resolve('charmingkamly.cn'));

// 正确用法 指定网址
var_dump((new Manager())->resolve('https://charmingkamly.cn/test.php'));

// 正确用法 指定网址 , 具体 DNS 服务器 和 相关参数
var_dump((new Manager())->resolve('https://charmingkamly.cn/test.php', [
'dns_ip' => '119.29.29.29',
'dns_port' => '53', // default: 53
'socket' => [
'rcv_time' => ['sec' => 0.5, 'usec' => 0], // 指定 socket 连接 接收超时时间
'snd_time' => ['sec' => 0.5, 'usec' => 0], // 指定 socket 连接 发送超时时间
],
'retry_time' => 3, // 尝试接受包的次数
]));

/*
* 具体 output 内容
array(4) {
["status"]=>
int(1)
["ra"]=>
int(1)
["resnum"]=>
int(1)
["list"]=>
array(1) {
[0]=>
array(3) {
["qtype"]=> // DNS协议的类型
int(1)
["ttl"]=> // time to live 表示资源记录可以缓存的时间
int(600)
["ip"]=> // 解析具体内容
string(15) "139.199.179.114"
}
}
}
*/

// 错误用法 指定具体 IP 地址
try {
(new Manager())->resolve('https://139.199.179.114/test.php'); // error reason
} catch (Exception $exception) {
var_dump($exception->getMessage());
}
// Domain is ip not allow

// 错误用法 DNS 服务器有误,默认使用 腾讯的公共 DNS 服务器 - 119.29.29.29
try {
(new Manager())->resolve('https://charmingkamly.cn/test.php', [
'dns_ip' => '192.168.1.2', // error reason
'dns_port' => 53,
'socket' => [
'rcv_time' => ['sec' => 0, 'usec' => 1],
'snd_time' => ['sec' => 0, 'usec' => 1],
]
]);
} catch (Exception $exception) {
var_dump($exception->getMessage());
}
// Resource temporarily unavailable
// [公共DNS可以在这里选其中一个 - 知乎](https://www.zhihu.com/question/32229915)
```

## 参考

[szulilin/php-dns](https://github.com/szulilin/php-dns)
24 changes: 24 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "kamly/domain-parser",
"description": "get host by name use ip",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "kamly",
"email": "lijiaming@charmingkamly.cn"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"psr-4": {
"Kamly\\DomainParser\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^7.5",
"mockery/mockery": "^1.2"
}
}
224 changes: 224 additions & 0 deletions src/DomainTcpParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?php

namespace Kamly\DomainParser;

/**
* 通过 socket 请求 指定 DNS 服务器,获取需要解析域名的 IP 地址
* Class Dns
*/
class DomainTcpParser

{
/** @var string */
private $domain;

/** @var int */
private $query_length = 0;

/**
* 初始化类
* DomainTcpParser constructor.
* @param $domain
* @throws \Exception
*/
public function __construct($domain)
{
$this->domain = Tool::normalizeDomain($domain); // 格式化 domain
}

/**
* 编码需要发送的内容,返回二进制内容
* @return mixed
*/
public function encode()
{
/*
* Header format 12b :
* Message ID 2b 0 - 65535-1 10进制
* FLAG => QR|opcode|AA|TC|RD|RA|Z|RCODE 2b 0x0100 16进制
* QDCOUNT 2b 0x0001
* ANCOUNT 2b 0x0000
* NSCOUNT 2b 0x0000
* ARCOUNT 2b 0x0000
*/
// 短整型(16位) 转 二进制
$domain_str = pack('n6', rand(1, 10000), 0x0100, 0x0001, 0x0000, 0x0000, 0x0000);
$this->query_length = 12; // 12字节,后面会直接乘8变成字 1b->8bit

/*
* Question
* QNAME
* 03 77 77 77 05 61 70 70 6c 65 03 63 6f 6d 00
* 3www5apple3com0
* QTYPE 0x0001
* QCLASS 0x0001
*/
$domain_array = explode(".", $this->domain);
foreach ($domain_array as $k => $v) {
$str_len = strlen($v);
$domain_str .= pack("C", $str_len); // 数字 转 二进制
$this->query_length += 1;
for ($i = 0; $i < $str_len; $i++) {
$char = ord($v[$i]); // 字符 转换 ASCII值
$domain_str .= pack('C', $char); // ASCII值 转 二进制
$this->query_length += 1;
}
}
$domain_str .= pack('C', 0); // 结束
$this->query_length += 1;

// 短整型(16位) 转 二进制
$domain_str .= pack("n2", 0x0001, 0x0001);
$this->query_length += 4;

$encode_len = $this->query_length * 8;

$encode['msg'] = $domain_str;
$encode['len'] = $encode_len;

// var_dump(bin2hex($encode['msg'])); // 二进制 转 十六进制

return $encode;
}

/**
* 解码收到的内容
* @param $code
* @return array
*/
public function decode($code)
{
// 设置默认返回值
$ret = [
'status' => 0,
'ra' => 0,
'resnum' => 0,
'list' => [],
];

$code = bin2hex($code); // 把二进制转为十六进制

// 前面都是一样,多了一个 Answer

// Message ID
$id = substr($code, 0, 4);

// FLAG
$flag = substr($code, 4, 4);
$flag = base_convert($flag, 16, 2); // 重新将十六进制字符串转换为二进制
// 判断最后 4bit是否为0。获取最后4位,然后从二进制转十进制进行判断。
$rcode = substr($flag, -4);
$rcode = base_convert($rcode, 2, 10);
if ($rcode != 0) {
return $ret; // 解析失败
}
$ret['status'] = 1;//解析成功

// 判断是否支持 支持递归查询。
$ret['ra'] = $flag[8] == 1 ? 1 : 0; // 1支持递归查询,0不支持递归查询

// 回答数量 ANCOUNT
$ret['resnum'] = hexdec(substr($code, 12, 4)); // 截取内容,然后十六进制转十进制

// query 的长度 1个b有2位所以乘2,
$query_length = $this->query_length * 2;

// 需要根据回答数量进行区分
for ($i = 0; $i < $ret['resnum']; $i++) {
$data_length = 0;

// 根据DNS协议的类型进行处理 十六进制转十进制
$qtype = hexdec(substr($code, $query_length + 4, 4));

switch ($qtype) {
case 1:
// DNS协议的类型
$ret['list'][$i]['qtype'] = 1;
// class in 表示RDATA的类
$class = hexdec(substr($code, $query_length + 8, 4));
// time to live 表示资源记录可以缓存的时间
$ret['list'][$i]['ttl'] = hexdec(substr($code, $query_length + 12, 8)); // 截取内容,然后十六进制转十进制
// data_length 表示RDATA的长度
$data_length = hexdec(substr($code, $query_length + 20, 4)); // 截取内容,然后十六进制转十进制
// rdata
$rdata_data = substr($code, $query_length + 24, $data_length * 2);

// 解析具体内容
$ret['list'][$i]['ip'] = $this->decode_ip($rdata_data);
break;
case 5:
// DNS协议的类型
$ret['list'][$i]['qtype'] = 5;
// class in 表示RDATA的类
$class = hexdec(substr($code, $query_length + 8, 4));
// time to live 表示资源记录可以缓存的时间
$ret['list'][$i]['ttl'] = hexdec(substr($code, $query_length + 12, 8)); // 截取内容,然后十六进制转十进制
// data_length 表示RDATA的长度
$data_length = hexdec(substr($code, $query_length + 20, 4)); // 截取内容,然后十六进制转十进制
// rdata
$rdata_data = substr($code, $query_length + 24, $data_length * 2);

// 解析具体内容
$ret['list'][$i]['cname'] = $this->decode_cname($code, $rdata_data);
break;
default:
break;
}
// 跟新 $query_length
$query_length = $query_length + 4 + 4 + 4 + 8 + 4 + $data_length * 2;
}
return $ret;
}

/**
* 解码cname,还不清楚原理
* @param $code
* @param $rdata_data
* @return string
*/
public function decode_cname($code, $rdata_data)
{
$domain = '';

for ($i = 0; $i < strlen($rdata_data); $i = $i + 2) {
$num = hexdec($rdata_data[$i] . $rdata_data[$i + 1]);
if (48 < $num && $num < 122) {
$domain .= pack("h*", $rdata_data[$i + 1] . $rdata_data[$i]); // 十六进制字符串 转 二进制
} else {
if ($rdata_data[$i + 1] . $rdata_data[$i] == '0c') {
// 存在标记位,在query里面解码内容
$position = hexdec($rdata_data[$i + 2] . $rdata_data[$i + 3]) * 2; // 开始位置
$offset = substr($code, $position); // 先截断
$end = strpos($offset, '00'); // 结束位置
$offset_domain = substr($code, $position, $end); // 截断最后的位置

for ($k = 0; $k < strlen($offset_domain); $k = $k + 2) {
$offset_num = hexdec($offset_domain[$k] . $offset_domain[$k + 1]);
if (48 < $offset_num && $offset_num < 122) {
$domain .= pack("h*", $offset_domain[$k + 1] . $offset_domain[$k]); // 十六进制字符串 转 二进制
} else {
$domain .= '.';
}
}
} else {
$domain .= '.';
}
}
}
return trim($domain, '.');
}

/**
* 解码IP
* @param $rdata_data
* @return string
*/
public function decode_ip($rdata_data)
{
$ip = '';
for ($i = 0; $i < strlen($rdata_data); $i = $i + 2) {
$ip .= hexdec($rdata_data[$i] . $rdata_data[$i + 1]) . '.';
}
return trim($ip, '.'); // 移除两侧
}
}
10 changes: 10 additions & 0 deletions src/Exceptions/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Kamly\DomainParser\Exceptions;

/**
* Class Exception.
*/
class Exception extends \Exception
{
}
Loading

0 comments on commit 23de647

Please sign in to comment.