Passed
Push — master ( d65721...d12e15 )
by luo
03:45
created

FreeNom::sendExceptionReport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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