FreeNom.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. /**
  3. * FreeNom域名自动续期
  4. *
  5. * @author mybsdc <[email protected]>
  6. * @date 2020/1/19
  7. * @time 17:29
  8. * @link https://github.com/luolongfei/freenom
  9. */
  10. namespace Luolongfei\App\Console;
  11. use Luolongfei\App\Exceptions\LlfException;
  12. use GuzzleHttp\Client;
  13. use GuzzleHttp\Cookie\CookieJar;
  14. use Luolongfei\Lib\Log;
  15. use Luolongfei\Lib\Mail;
  16. use Luolongfei\Lib\TelegramBot;
  17. class FreeNom
  18. {
  19. const VERSION = 'v0.3';
  20. const TIMEOUT = 34.52;
  21. // FreeNom登录地址
  22. const LOGIN_URL = 'https://my.freenom.com/dologin.php';
  23. // 域名状态地址
  24. const DOMAIN_STATUS_URL = 'https://my.freenom.com/domains.php?a=renewals';
  25. // 域名续期地址
  26. const RENEW_DOMAIN_URL = 'https://my.freenom.com/domains.php?submitrenewals=true';
  27. // 匹配token的正则
  28. const TOKEN_REGEX = '/name="token"\svalue="(?P<token>[^"]+)"/i';
  29. // 匹配域名信息的正则
  30. const DOMAIN_INFO_REGEX = '/<tr><td>(?P<domain>[^<]+)<\/td><td>[^<]+<\/td><td>[^<]+<span class="[^"]+">(?P<days>\d+)[^&]+&domain=(?P<id>\d+)"/i';
  31. // 匹配登录状态的正则
  32. const LOGIN_STATUS_REGEX = '/<li.*?Logout.*?<\/li>/i';
  33. /**
  34. * @var FreeNom
  35. */
  36. protected static $instance;
  37. /**
  38. * @var Client
  39. */
  40. protected $client;
  41. /**
  42. * @var CookieJar | bool
  43. */
  44. protected $jar = true;
  45. /**
  46. * @var string freenom账户
  47. */
  48. protected $username;
  49. /**
  50. * @var string freenom密码
  51. */
  52. protected $password;
  53. public function __construct()
  54. {
  55. $this->client = new Client([
  56. 'headers' => [
  57. 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  58. 'Accept-Encoding' => 'gzip, deflate, br',
  59. 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
  60. ],
  61. 'timeout' => self::TIMEOUT,
  62. CURLOPT_FOLLOWLOCATION => true,
  63. CURLOPT_AUTOREFERER => true,
  64. 'verify' => config('verifySSL'),
  65. 'debug' => config('debug')
  66. ]);
  67. system_log(sprintf('当前程序版本 %s', self::VERSION));
  68. }
  69. /**
  70. * @return FreeNom
  71. */
  72. public static function instance()
  73. {
  74. if (!self::$instance instanceof self) {
  75. self::$instance = new self();
  76. }
  77. return self::$instance;
  78. }
  79. /**
  80. * 登录
  81. */
  82. protected function login()
  83. {
  84. $this->client->post(self::LOGIN_URL, [
  85. 'headers' => [
  86. 'Content-Type' => 'application/x-www-form-urlencoded',
  87. 'Referer' => 'https://my.freenom.com/clientarea.php'
  88. ],
  89. 'form_params' => [
  90. 'username' => $this->username,
  91. 'password' => $this->password
  92. ],
  93. 'cookies' => $this->jar
  94. ]);
  95. }
  96. /**
  97. * 续期
  98. *
  99. * @throws \Exception
  100. * @throws LlfException
  101. */
  102. public function renewDomains()
  103. {
  104. // 所有请求共用一个CookieJar实例
  105. $this->jar = new CookieJar();
  106. $this->login();
  107. $authCookie = $this->jar->getCookieByName('WHMCSZH5eHTGhfvzP')->getValue();
  108. if (empty($authCookie)) {
  109. throw new LlfException(34520002);
  110. }
  111. // 检查域名状态
  112. $response = $this->client->get(self::DOMAIN_STATUS_URL, [
  113. 'headers' => [
  114. 'Referer' => 'https://my.freenom.com/clientarea.php'
  115. ],
  116. 'cookies' => $this->jar
  117. ]);
  118. $body = (string)$response->getBody();
  119. if (!preg_match(self::LOGIN_STATUS_REGEX, $body)) {
  120. throw new LlfException(34520009);
  121. }
  122. // 域名数据
  123. if (!preg_match_all(self::DOMAIN_INFO_REGEX, $body, $domains, PREG_SET_ORDER)) {
  124. throw new LlfException(34520003);
  125. }
  126. // 页面token
  127. if (!preg_match(self::TOKEN_REGEX, $body, $matches)) {
  128. throw new LlfException(34520004);
  129. }
  130. $token = $matches['token'];
  131. // 续期
  132. $result = '';
  133. $renewed = $renewedTG = ''; // 续期成功的域名
  134. $notRenewed = $notRenewedTG = ''; // 记录续期出错的域名,用于邮件通知内容
  135. $domainInfo = $domainInfoTG = ''; // 域名状态信息,用于邮件通知内容
  136. foreach ($domains as $d) {
  137. $domain = $d['domain'];
  138. $days = intval($d['days']);
  139. $id = $d['id'];
  140. // 免费域名只允许在到期前14天内续期
  141. if ($days <= 14) {
  142. try {
  143. $response = $this->client->post(self::RENEW_DOMAIN_URL, [
  144. 'headers' => [
  145. 'Referer' => sprintf('https://my.freenom.com/domains.php?a=renewdomain&domain=%s', $id),
  146. 'Content-Type' => 'application/x-www-form-urlencoded'
  147. ],
  148. 'form_params' => [
  149. 'token' => $token,
  150. 'renewalid' => $id,
  151. sprintf('renewalperiod[%s]', $id) => '12M', // 续期一年
  152. 'paymentmethod' => 'credit', // 支付方式:信用卡
  153. ],
  154. 'cookies' => $this->jar
  155. ]);
  156. } catch (\Exception $e) {
  157. system_log(sprintf('%s:续期请求出错:%s', $this->username, $e->getMessage()));
  158. continue;
  159. }
  160. $body = (string)$response->getBody();
  161. sleep(1);
  162. if (stripos($body, 'Order Confirmation') === false) { // 续期失败
  163. $result .= sprintf("%s续期失败\n", $domain);
  164. $notRenewed .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>', $domain, $domain);
  165. $notRenewedTG .= sprintf('[%s](http://%s) ', $domain, $domain);
  166. } else {
  167. $result .= sprintf("%s续期成功\n", $domain);
  168. $renewed .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>', $domain, $domain);
  169. $renewedTG .= sprintf('[%s](http://%s) ', $domain, $domain);
  170. continue;
  171. }
  172. }
  173. $domainInfo .= sprintf('<a href="http://%s" rel="noopener" target="_blank">%s</a>还有<span style="font-weight: bold; font-size: 16px;">%d</span>天到期,', $domain, $domain, $days);
  174. $domainInfoTG .= sprintf('[%s](http://%s)还有*%d*天到期,', $domain, $domain, $days);
  175. }
  176. $domainInfoTG .= "更多信息可以参考[Freenom官网](https://my.freenom.com/domains.php?a=renewals)哦~\n\n(如果你不想每次执行都收到推送,请将 .env 中 NOTICE_FREQ 的值设为0,使程序只在有续期操作时才推送)";
  177. if ($notRenewed || $renewed) {
  178. Mail::send(
  179. '主人,我刚刚帮你续期域名啦~',
  180. [
  181. $this->username,
  182. $renewed ? sprintf('续期成功:%s<br>', $renewed) : '',
  183. $notRenewed ? sprintf('续期出错:%s<br>', $notRenewed) : '',
  184. $domainInfo ?: '哦豁,没看到其它域名。'
  185. ]
  186. );
  187. TelegramBot::send(sprintf(
  188. "主人,我刚刚帮你续期域名啦~\n\n%s%s\n另外,%s",
  189. $renewedTG ? sprintf("续期成功:%s\n", $renewedTG) : '',
  190. $notRenewedTG ? sprintf("续期失败:%s\n", $notRenewedTG) : '',
  191. $domainInfoTG
  192. ));
  193. system_log(sprintf("%s:续期结果如下:\n%s", $this->username, $result));
  194. } else {
  195. if (config('noticeFreq') == 1) {
  196. Mail::send(
  197. '报告,今天没有域名需要续期',
  198. [
  199. $this->username,
  200. $domainInfo
  201. ],
  202. '',
  203. 'notice'
  204. );
  205. TelegramBot::send("报告,今天没有域名需要续期,所有域名情况如下:\n\n" . $domainInfoTG);
  206. } else {
  207. system_log('当前通知频率为「仅当有续期操作时」,故本次不会推送通知');
  208. }
  209. system_log(sprintf('%s:<green>执行成功,今次没有需要续期的域名</green>', $this->username));
  210. }
  211. }
  212. /**
  213. * 二维数组去重
  214. *
  215. * @param array $array 原始数组
  216. * @param array $keys 可指定对应的键联合
  217. *
  218. * @return bool
  219. */
  220. public function arrayUnique(array &$array, array $keys = [])
  221. {
  222. if (!isset($array[0]) || !is_array($array[0])) {
  223. return false;
  224. }
  225. if (empty($keys)) {
  226. $keys = array_keys($array[0]);
  227. }
  228. $tmp = [];
  229. foreach ($array as $k => $items) {
  230. $combinedKey = '';
  231. foreach ($keys as $key) {
  232. $combinedKey .= $items[$key];
  233. }
  234. if (isset($tmp[$combinedKey])) {
  235. unset($array[$k]);
  236. } else {
  237. $tmp[$combinedKey] = $k;
  238. }
  239. }
  240. unset($tmp);
  241. return true;
  242. }
  243. /**
  244. * 获取freenom账户信息
  245. *
  246. * @return array
  247. * @throws LlfException
  248. */
  249. protected function getAccounts()
  250. {
  251. $accounts = [];
  252. $multipleAccounts = preg_replace('/\s/', '', env('MULTIPLE_ACCOUNTS'));
  253. if (preg_match_all('/<(?P<u>.*?)>@<(?P<p>.*?)>/i', $multipleAccounts, $matches, PREG_SET_ORDER)) {
  254. foreach ($matches as $m) {
  255. $accounts[] = [
  256. 'username' => $m['u'],
  257. 'password' => $m['p']
  258. ];
  259. }
  260. }
  261. $username = env('FREENOM_USERNAME');
  262. $password = env('FREENOM_PASSWORD');
  263. if ($username && $password) {
  264. $accounts[] = [
  265. 'username' => $username,
  266. 'password' => $password
  267. ];
  268. }
  269. if (empty($accounts)) {
  270. throw new LlfException(34520001);
  271. }
  272. // 去重
  273. $this->arrayUnique($accounts);
  274. return $accounts;
  275. }
  276. /**
  277. * 发送异常报告
  278. *
  279. * @param \Exception $e
  280. *
  281. * @throws \Exception
  282. */
  283. private function sendExceptionReport($e)
  284. {
  285. Mail::send(
  286. '主人,' . $e->getMessage(),
  287. [
  288. $this->username,
  289. sprintf('具体是在%s文件的第%d行,抛出了一个异常。异常的内容是%s,快去看看吧。', $e->getFile(), $e->getLine(), $e->getMessage()),
  290. ],
  291. '',
  292. 'LlfException'
  293. );
  294. TelegramBot::send(sprintf(
  295. '主人,出错了。具体是在%s文件的第%d行,抛出了一个异常。异常的内容是%s,快去看看吧。(账户:%s)',
  296. $e->getFile(),
  297. $e->getLine(),
  298. $e->getMessage(),
  299. $this->username
  300. ), '', false);
  301. }
  302. /**
  303. * @throws LlfException
  304. * @throws \Exception
  305. */
  306. public function handle()
  307. {
  308. $accounts = $this->getAccounts();
  309. foreach ($accounts as $account) {
  310. try {
  311. $this->username = $account['username'];
  312. $this->password = $account['password'];
  313. $this->renewDomains();
  314. } catch (LlfException $e) {
  315. system_log(sprintf('出错:<red>%s</red>', $e->getMessage()));
  316. $this->sendExceptionReport($e);
  317. } catch (\Exception $e) {
  318. system_log(sprintf('出错:<red>%s</red>', $e->getMessage()), $e->getTrace());
  319. $this->sendExceptionReport($e);
  320. }
  321. }
  322. }
  323. }