Issues (33)

app/Console/FreeNom.php (1 issue)

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