diff --git a/lib/routes/cma/channel.tsx b/lib/routes/cma/channel.tsx index abdcf0c96..f8920a830 100644 --- a/lib/routes/cma/channel.tsx +++ b/lib/routes/cma/channel.tsx @@ -1,12 +1,51 @@ +import { createHash } from 'node:crypto'; + import { load } from 'cheerio'; import { raw } from 'hono/html'; import { renderToString } from 'hono/jsx/dom/server'; import type { Route } from '@/types'; import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; +const solveChallenge = (prefix: string, leadingZeroBits: number) => { + for (let count = 0; count < 100_000_000; count++) { + const suffix = count.toString(16); + const hash = createHash('sha1') + .update(prefix + suffix) + .digest(); + if (hash.readUInt32BE(0) >>> (32 - leadingZeroBits) === 0) { + return suffix; + } + } + throw new Error('Failed to solve the SafeLine challenge of weather.cma.cn'); +}; + +const fetchPageWithChallenge = async (url: string) => { + const response = await ofetch.raw(url); + const html = response._data ?? ''; + const challenge = response.headers + .getSetCookie() + .find((cookie) => cookie.startsWith('safeline_bot_challenge=')) + ?.split(';', 1)[0]; + const prefix = html.match(/var prefix = '(\w+)';/)?.[1]; + const leadingZeroBits = Number(html.match(/var leading_zero_bit = (\d+);/)?.[1]); + + if (!challenge || !prefix || !leadingZeroBits) { + return html; + } + + const answer = challenge.replace('safeline_bot_challenge=', 'safeline_bot_challenge_ans=') + solveChallenge(prefix, leadingZeroBits); + + return ofetch(url, { + headers: { + Cookie: `${challenge}; ${answer}`, + }, + }); +}; + export const route: Route = { path: '/channel/:id?', categories: ['forecast'], @@ -15,7 +54,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: false, + antiCrawler: true, supportBT: false, supportPodcast: false, supportScihub: false, @@ -65,11 +104,11 @@ async function handler(ctx) { }, }); - const data = response?.data?.pop() ?? {}; + const article = response?.data?.pop() ?? {}; - data.image = data.image?.replace(/\?.*$/, '') ?? undefined; + article.image = article.image?.replace(/\?.*$/, ''); - const { data: currentResponse } = await got(currentUrl); + const currentResponse = await fetchPageWithChallenge(currentUrl); const $ = load(currentResponse); @@ -84,38 +123,32 @@ async function handler(ctx) { const descriptionHtml = $('div.xml').html(); const image = new URL($('li.active a img').prop('src'), rootUrl).href; const icon = new URL($('link[rel="shortcut icon"]').prop('href'), rootUrl).href; + const sourceElement = $('div.col-xs-8 span') + .toArray() + .findLast((element) => $(element).text().startsWith('来源')); + const itemAuthor = $(sourceElement).text().split(':').pop() || author; - const items = data - ? [ - { - title: `${data.title} ${data.releaseTime}`, - link: new URL(data.link, rootUrl).href, - description: renderToString( - <> - {data.image ? ( -
- {data.title} -
- ) : null} - {descriptionHtml ? raw(descriptionHtml) : null} - - ), - author: - $( - $('div.col-xs-8 span') - .toArray() - .findLast((a) => $(a).text().startsWith('来源')) - ) - ?.text() - ?.split(/:/) - ?.pop() || author, - guid: `cma${data.link}#${data.releaseTime.replaceAll(/\s/g, '-')}`, - pubDate: timezone(parseDate(data.releaseTime), 8), - enclosure_url: new URL(data.image, rootUrl).href, - enclosure_type: data.image ? `image/${data.image.split(/\./).pop()}` : undefined, - }, - ] - : []; + const items = [ + { + title: `${article.title} ${article.releaseTime}`, + link: new URL(article.link, rootUrl).href, + description: renderToString( + <> + {article.image ? ( +
+ {article.title} +
+ ) : null} + {descriptionHtml ? raw(descriptionHtml) : null} + + ), + author: itemAuthor, + guid: `cma${article.link}#${article.releaseTime.replaceAll(/\s/g, '-')}`, + pubDate: timezone(parseDate(article.releaseTime), 8), + enclosure_url: new URL(article.image, rootUrl).href, + enclosure_type: article.image ? `image/${article.image.split(/\./).pop()}` : undefined, + }, + ]; return { item: items,