* @date 2020/1/19 * @time 17:29 * @link https://github.com/luolongfei/freenom */ namespace Luolongfei\App\Console; use Luolongfei\App\Exceptions\LlfException; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use Luolongfei\Libs\Log; use Luolongfei\Libs\Message; use Luolongfei\Libs\MessageServices\Mail; use Luolongfei\Libs\MessageServices\TelegramBot; class FreeNom { const VERSION = 'v0.4'; const TIMEOUT = 33; // FreeNom登录地址 const LOGIN_URL = 'https://my.freenom.com/dologin.php'; // 域名状态地址 const DOMAIN_STATUS_URL = 'https://my.freenom.com/domains.php?a=renewals'; // 域名续期地址 const RENEW_DOMAIN_URL = 'https://my.freenom.com/domains.php?submitrenewals=true'; // 匹配token的正则 const TOKEN_REGEX = '/name="token"\svalue="(?P[^"]+)"/i'; // 匹配域名信息的正则 const DOMAIN_INFO_REGEX = '/(?P[^<]+)<\/td>[^<]+<\/td>[^<]+(?P\d+)[^&]+&domain=(?P\d+)"/i'; // 匹配登录状态的正则 const LOGIN_STATUS_REGEX = '//i'; /** * @var FreeNom */ protected static $instance; /** * @var Client */ protected $client; /** * @var CookieJar | bool */ protected $jar = true; /** * @var string FreeNom 账户 */ protected $username; /** * @var string FreeNom 密码 */ protected $password; public function __construct() { $this->client = new Client([ 'headers' => [ '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', 'Accept-Encoding' => 'gzip, deflate, br', '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', ], 'timeout' => self::TIMEOUT, CURLOPT_FOLLOWLOCATION => true, CURLOPT_AUTOREFERER => true, 'verify' => config('verify_ssl'), 'debug' => config('debug') ]); system_log(sprintf('当前程序版本 %s', self::VERSION)); } /** * @return FreeNom */ public static function getInstance() { if (!self::$instance instanceof self) { self::$instance = new self(); } return self::$instance; } /** * 登录 * * @param string $username * @param string $password * * @return bool * @throws LlfException */ protected function login(string $username, string $password) { try { $this->client->post(self::LOGIN_URL, [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', 'Referer' => 'https://my.freenom.com/clientarea.php' ], 'form_params' => [ 'username' => $username, 'password' => $password ], 'cookies' => $this->jar ]); } catch (\Exception $e) { throw new LlfException(34520002, $e->getMessage()); } if (empty($this->jar->getCookieByName('WHMCSZH5eHTGhfvzP')->getValue())) { throw new LlfException(34520002, lang('error_msg.100001')); } return true; } /** * 匹配获取所有域名 * * @param string $domainStatusPage * * @return array * @throws LlfException */ protected function getAllDomains(string $domainStatusPage) { if (!preg_match_all(self::DOMAIN_INFO_REGEX, $domainStatusPage, $allDomains, PREG_SET_ORDER)) { throw new LlfException(34520003); } return $allDomains; } /** * 获取匹配 token * * 据观察,每次登录后此 token 不会改变,故可以只获取一次,多次使用 * * @param string $domainStatusPage * * @return string * @throws LlfException */ protected function getToken(string $domainStatusPage) { if (!preg_match(self::TOKEN_REGEX, $domainStatusPage, $matches)) { throw new LlfException(34520004); } return $matches['token']; } /** * 获取域名状态页面 * * @return string * @throws LlfException */ protected function getDomainStatusPage() { try { $resp = $this->client->get(self::DOMAIN_STATUS_URL, [ 'headers' => [ 'Referer' => 'https://my.freenom.com/clientarea.php' ], 'cookies' => $this->jar ]); $page = (string)$resp->getBody(); } catch (\Exception $e) { throw new LlfException(34520013, $e->getMessage()); } if (!preg_match(self::LOGIN_STATUS_REGEX, $page)) { throw new LlfException(34520009); } return $page; } /** * 续期所有域名 * * @param array $allDomains * @param string $token * * @return bool */ public function renewAllDomains(array $allDomains, string $token) { $renewalSuccessArr = []; $renewalFailuresArr = []; $domainStatusArr = []; foreach ($allDomains as $d) { $domain = $d['domain']; $days = (int)$d['days']; $id = $d['id']; // 免费域名只允许在到期前 14 天内续期 if ($days <= 14) { $renewalResult = $this->renew($id, $token); sleep(1); if ($renewalResult) { $renewalSuccessArr[] = $domain; continue; // 续期成功的域名无需记录过期天数 } else { $renewalFailuresArr[] = $domain; } } // 记录域名过期天数 $domainStatusArr[$domain] = $days; } // 存在续期操作 if ($renewalSuccessArr || $renewalFailuresArr) { $data = [ 'username' => $this->username, 'renewalSuccessArr' => $renewalSuccessArr, 'renewalFailuresArr' => $renewalFailuresArr, 'domainStatusArr' => $domainStatusArr, ]; Message::send('', '主人,我刚刚帮你续期域名啦~', 2, $data); system_log(sprintf( '恭喜,成功续期 %d 个域名,失败 %d 个域名,详细的续期结果已送信成功,请注意查收', count($renewalSuccessArr), count($renewalFailuresArr) )); Log::info(sprintf("账户:%s\n续期结果如下:\n", $this->username), $data); return true; } // 不存在续期操作 if (config('notice_freq') === 1) { $data = [ 'username' => $this->username, 'domainStatusArr' => $domainStatusArr, ]; Message::send('', '报告,今天没有域名需要续期', 3, $data); system_log('域名状态信息已送信成功,请注意查收'); } else { system_log('当前通知频率为「仅当有续期操作时」,故本次不会推送通知'); } system_log(sprintf('%s:执行成功,今次没有需要续期的域名', $this->username)); return true; } /** * 续期单个域名 * * @param int $id * @param string $token * * @return bool */ protected function renew(int $id, string $token) { try { $resp = $this->client->post(self::RENEW_DOMAIN_URL, [ 'headers' => [ 'Referer' => sprintf('https://my.freenom.com/domains.php?a=renewdomain&domain=%s', $id), 'Content-Type' => 'application/x-www-form-urlencoded' ], 'form_params' => [ 'token' => $token, 'renewalid' => $id, sprintf('renewalperiod[%s]', $id) => '12M', // 续期一年 'paymentmethod' => 'credit', // 支付方式:信用卡 ], 'cookies' => $this->jar ]); $resp = (string)$resp->getBody(); return stripos($resp, 'Order Confirmation') !== false; } catch (\Exception $e) { $errorMsg = sprintf('续期请求出错:%s,域名 ID:%s(账户:%s)', $e->getMessage(), $id, $this->username); system_log($errorMsg); Message::send($errorMsg); return false; } } /** * 二维数组去重 * * @param array $array 原始数组 * @param array $keys 可指定对应的键联合 * * @return bool */ public function arrayUnique(array &$array, array $keys = []) { if (!isset($array[0]) || !is_array($array[0])) { return false; } if (empty($keys)) { $keys = array_keys($array[0]); } $tmp = []; foreach ($array as $k => $items) { $combinedKey = ''; foreach ($keys as $key) { $combinedKey .= $items[$key]; } if (isset($tmp[$combinedKey])) { unset($array[$k]); } else { $tmp[$combinedKey] = $k; } } unset($tmp); return true; } /** * 获取 FreeNom 账户信息 * * @return array * @throws LlfException */ protected function getAccounts() { $accounts = []; $multipleAccounts = preg_replace('/\s/', '', env('MULTIPLE_ACCOUNTS')); if (preg_match_all('/<(?P.*?)>@<(?P

.*?)>/i', $multipleAccounts, $matches, PREG_SET_ORDER)) { foreach ($matches as $m) { $accounts[] = [ 'username' => $m['u'], 'password' => $m['p'] ]; } } $username = env('FREENOM_USERNAME'); $password = env('FREENOM_PASSWORD'); if ($username && $password) { $accounts[] = [ 'username' => $username, 'password' => $password ]; } if (empty($accounts)) { throw new LlfException(34520001); } // 去重 $this->arrayUnique($accounts); return $accounts; } /** * 发送异常报告 * * @param $e \Exception|LlfException */ private function sendExceptionReport($e) { Message::send(sprintf( '具体是在%s文件的第%d行,抛出了一个异常。异常的内容是%s,快去看看吧。(账户:%s)', $e->getFile(), $e->getLine(), $e->getMessage(), $this->username ), '主人,出错了,' . $e->getMessage()); } /** * @throws LlfException * @throws \Exception */ public function handle() { $accounts = $this->getAccounts(); foreach ($accounts as $account) { try { $this->username = $account['username']; $this->password = $account['password']; $this->jar = new CookieJar(); // 所有请求共用一个 CookieJar 实例 $this->login($this->username, $this->password); $domainStatusPage = $this->getDomainStatusPage(); $allDomains = $this->getAllDomains($domainStatusPage); $token = $this->getToken($domainStatusPage); $this->renewAllDomains($allDomains, $token); } catch (LlfException $e) { system_log(sprintf('出错:%s', $e->getMessage())); $this->sendExceptionReport($e); } catch (\Exception $e) { system_log(sprintf('出错:%s', $e->getMessage()), $e->getTrace()); $this->sendExceptionReport($e); } } } }