Charles代理是一个HTTP调试代理,可以检查网络调用和调试SSL流量。使用Charles,你能够检查请求/响应、头信息和cookies。今天我们将看到如何设置Charles,以及如何使用查尔斯代理进行网页爬取。我们将专注于从Javascript重的网页和移动应用程序中提取数据。Charles位于你的应用程序和互联网之间。
Charles就像类固醇的Chrome开发工具。它有许多令人难以置信的功能。
- 带宽节流以模拟缓慢的互联网连接
- 请求编辑(例如,测试后端API的行为)。
- 重复的请求
- 对请求列表进行全文搜索(对网络抓取非常有趣)。
- 更多
在调试单页面应用程序或移动应用程序时,Charles是一个非常有趣的工具。
安装和设置
我们将看到如何在macOS上安装和配置Charles,但也有一个Windows和Linux版本。
- 首先访问https://www.charlesproxy.com/download/,下载Charles。
- 安装Charles后,你需要安装其根证书。这将使查尔斯能够拦截和解密SSL流量。
- 点击安装根证书后,它将打开你的macOS钥匙串访问,你将不得不打开信任菜单并点击 “总是信任”。
- 这将要求你提供你的系统密码。
- 现在你需要进入代理 > SSL代理设置,并添加 “*”或你想检查SSL的域名。
Charles现在将捕获你所选择的域名上的HTTPS流量。
在单页面应用中寻找隐藏的API
在传统的服务器端渲染的网站中,HTML代码由后台服务构建,完整的页面被返回到HTTP客户端(一般是你的浏览器)。 在过去的10年中,越来越多的网站使用单页应用程序框架(如React.js、Vue.js或Angular)进行客户端渲染。
这些框架正在向后端API发送许多请求,直接使用这些API而不是爬取网站,用无头浏览器渲染Javascript来提取你想要的数据可能是一个好主意。这将会快得多,你不需要昂贵的硬件(无头浏览器需要大量的内存和强大的CPU)。
我们将看看不同的单页应用程序,看看Charles代理如何帮助你从后端API发现和提取数据。
让我们从ProductHunt开始
ProductHunt是一个著名的在线发布产品的网站。它在科技生态系统中非常受欢迎。每天都有几十个项目推出,所以首页只加载当天的产品。有一个无限的滚动来查看前几天的产品。
我们将使用Charles代理来分析后端API调用,并用一些Python代码来重现它。
现在打开Charles代理,进入Producthunt主页,滚动几次到页面底部。
默认情况下,Charles会捕捉你的系统发出的每个HTTP请求,而不仅仅是你的浏览器。因此,你会得到大量的 “污染”。你可以通过输入你感兴趣的域来过滤这些。
如果你点击其中一个到/frontend/graphql
端点的POST请求,你可以检查请求和响应。
这些请求有很多事情要做,但如果你比较一下被发送到GraphQL API的内容,似乎唯一改变的是cursor
参数。
它是base64编码的,但幸运的是,Charles有一个功能可以快速解码Base64内容。你只需选择它并右键点击。
Charles提供的一个杀手锏是能够编辑任何请求并重播。在我们的案例中,这真的很好,因为在请求里面有很多头信息/cookies值,所以用HTTP客户端或在你的代码里面试图重现这个请求将是一场噩梦。
为了做到这一点,你可以在请求上点击右键,然后点击Compose。
然后你可以玩玩cursor
值,用base64编码的不同页码来替换它。然后点击 “执行”。
你也可以尝试删除不同的cookies(会有效果)。这是个好消息,因为如果使用这个端点必须要有cookie,那么用我们的Python代码重现这个请求就会更加复杂。
现在你可以把请求导出到cURL。
然后你可以使用像这样的工具,将cURL命令转换为Python代码(使用奇妙的Requests包):https://curl.trillworks.com/
我只是添加了verify=False
参数,以避免Request的SSL警告:
import requests headers = { 'Host': 'www.producthunt.com', 'accept': '*/*', 'x-requested-with': 'XMLHttpRequest', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36', 'content-type': 'application/json', 'origin': 'https://www.producthunt.com', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://www.producthunt.com/', 'accept-language': 'en-US,en;q=0.9', } data = '{"operationName":"HomePage","variables":{"cursor":"OA==","featured":true,"includePromotedPost":false,"visibleOnHomepage":true,"includeLayout":false},"query":"query HomePage($cursor: String, $postCursor: String, $featured: Boolean!, $includePromotedPost: Boolean!, $visibleOnHomepage: Boolean!) {\\n sections(first: 1, after: $cursor, featured: $featured) {\\n edges {\\n cursor\\n node {\\n id\\n date\\n cutoff_index\\n posts_count\\n cards(first: 1, after: $cursor) {\\n edges {\\n node {\\n ...FeedCards\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n posts(after: $postCursor, visible_on_homepage: $visibleOnHomepage) {\\n edges {\\n node {\\n ...PostItemList\\n featured_comment {\\n id\\n body: body_text\\n user {\\n id\\n ...UserImageLink\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n pageInfo {\\n endCursor\\n hasNextPage\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n pageInfo {\\n endCursor\\n hasNextPage\\n __typename\\n }\\n __typename\\n }\\n ad(kind: \\"feed\\") @include(if: $includePromotedPost) {\\n ...AdFragment\\n __typename\\n }\\n promoted_email_campaign(promoted_type: HOMEPAGE) @include(if: $includePromotedPost) {\\n id\\n abTestName\\n abVariant {\\n id\\n ...PromotedEmailAbTestVariantFragment\\n __typename\\n }\\n ...PromotedEmailCampaignFragment\\n __typename\\n }\\n daily_newsletter {\\n id\\n subject\\n __typename\\n }\\n viewer {\\n id\\n email\\n has_newsletter_subscription\\n __typename\\n }\\n ph_homepage_og_image_url\\n}\\n\\nfragment FeedCards on Card {\\n ...NewPostsCard\\n ...BestProductsFromLastWeekCard\\n ...MakersDiscussionCardFragment\\n ...GoldenKittyCardFragment\\n __typename\\n}\\n\\nfragment NewPostsCard on NewPostsCard {\\n is_dismissed\\n kind\\n posts {\\n ...PostItemList\\n __typename\\n }\\n __typename\\n}\\n\\nfragment PostItemList on Post {\\n id\\n ...PostItem\\n __typename\\n}\\n\\nfragment PostItem on Post {\\n id\\n _id\\n comments_count\\n name\\n shortened_url\\n slug\\n tagline\\n updated_at\\n topics {\\n edges {\\n node {\\n id\\n name\\n slug\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n ...PostThumbnail\\n ...PostVoteButton\\n __typename\\n}\\n\\nfragment PostThumbnail on Post {\\n id\\n name\\n thumbnail {\\n id\\n media_type\\n ...MediaThumbnail\\n __typename\\n }\\n ...PostStatusIcons\\n __typename\\n}\\n\\nfragment MediaThumbnail on Media {\\n id\\n image_uuid\\n __typename\\n}\\n\\nfragment PostStatusIcons on Post {\\n name\\n product_state\\n __typename\\n}\\n\\nfragment PostVoteButton on Post {\\n _id\\n id\\n featured_at\\n updated_at\\n created_at\\n disabled_when_scheduled\\n has_voted\\n ... on Votable {\\n id\\n votes_count\\n __typename\\n }\\n __typename\\n}\\n\\nfragment BestProductsFromLastWeekCard on BestProductsFromLastWeekCard {\\n posts {\\n ...PostItemList\\n __typename\\n }\\n __typename\\n}\\n\\nfragment MakersDiscussionCardFragment on MakersDiscussionCard {\\n isDismissed\\n discussion {\\n _id\\n id\\n ...DiscussionThreadListItem\\n __typename\\n }\\n __typename\\n}\\n\\nfragment DiscussionThreadListItem on DiscussionThread {\\n _id\\n id\\n title\\n description\\n descriptionHtml\\n slug\\n commentsCount\\n can_comment: canComment\\n discussionPath\\n canEdit\\n votesCount\\n hasVoted\\n createdAt\\n poll {\\n ...PollFragment\\n __typename\\n }\\n user {\\n id\\n name\\n username\\n headline\\n avatar\\n __typename\\n }\\n __typename\\n}\\n\\nfragment PollFragment on Poll {\\n id\\n answersCount\\n hasAnswered\\n options {\\n id\\n text\\n imageUuid\\n answersCount\\n answersPercent\\n hasAnswered\\n __typename\\n }\\n __typename\\n}\\n\\nfragment GoldenKittyCardFragment on GoldenKittyCard {\\n is_dismissed\\n category_for_voting {\\n id\\n slug\\n __typename\\n }\\n __typename\\n}\\n\\nfragment PromotedEmailCampaignFragment on PromotedEmailCampaign {\\n id\\n _id\\n title\\n tagline\\n thumbnail\\n ctaText\\n __typename\\n}\\n\\nfragment PromotedEmailAbTestVariantFragment on PromotedEmailAbTestVariant {\\n id\\n _id\\n title\\n tagline\\n thumbnail\\n ctaText\\n __typename\\n}\\n\\nfragment AdFragment on LegacyAdsUnion {\\n ... on PromotedPost {\\n id\\n ...LegacyPromotedPostItem\\n __typename\\n }\\n ... on AdChannel {\\n id\\n post {\\n id\\n slug\\n name\\n updated_at\\n comments_count\\n ...PostVoteButton\\n __typename\\n }\\n ctaText\\n dealText\\n adName: name\\n adTagline: tagline\\n adThumbnailUuid: thumbnailUuid\\n adUrl: url\\n __typename\\n }\\n __typename\\n}\\n\\nfragment LegacyPromotedPostItem on PromotedPost {\\n id\\n deal\\n post {\\n id\\n ...PostItem\\n __typename\\n }\\n name\\n tagline\\n ctaText\\n url\\n thumbnailUuid\\n ...ViewableImpressionSubject\\n __typename\\n}\\n\\nfragment ViewableImpressionSubject on Node {\\n id\\n __typename\\n}\\n\\nfragment UserImageLink on User {\\n id\\n _id\\n name\\n username\\n avatar\\n ...UserImage\\n __typename\\n}\\n\\nfragment UserImage on User {\\n id\\n post_upvote_streak\\n name\\n avatar\\n __typename\\n}\\n"}' response = requests.post('https://www.producthunt.com/frontend/graphql', headers=headers, cookies=cookies, data=data, verify=False)
然后就可以了!你现在可以通过调整光标参数来获得你想要的页码,你只需要把它编码成base64。
移动应用逆向工程
在网络应用中检查/修改网络调用的能力是非常酷的。但你知道什么更酷吗?是的,移动应用程序!这是很重要的。当然,它可能在服务条款中被禁止,所以在对移动应用程序进行任何请求分析之前,请确保阅读它。
几年前,让Charles代理在你的移动设备上工作有点复杂,你必须使用你的桌面Charles作为代理,在同一个wifi网络上等等。自2018年以来,他们推出了一个原生的iOS应用程序,你可以在应用程序商店下载:https://apps.apple.com/us/app/charles-proxy/id1134218562
现在,让Charles拦截你最喜欢的应用程序的HTTPS流量更容易了。安装该应用程序,然后在主屏幕上激活它。
一旦你激活了Charles并设置了VPN配置,你需要进入设置屏幕并启用SSL代理(只需按照说明操作)。
当你打开SSL代理时,一些应用程序可能会停止工作。当你安装Charles时,你也在安装一个根证书,以便Charles可以检查你的SSL流量。
这有点像 “中间人 “攻击(但在这种情况下你是在攻击你自己)。
一些应用程序正在验证根证书,而不会接受Charles的根证书。这就是所谓的SSL钉牢。
我们要分析的iOS应用程序是https://dev.to :https://apps.apple.com/us/app/dev-community/id1439094790。
Dev.to是一个伟大的开发者社区网站,人们可以在这里发表文章。这个应用程序非常直接,它允许在dev.to上以原生体验的方式进行阅读。
现在打开激活Charles的应用程序,并滚动几次以加载更多的文章。
与Charles桌面应用程序一样,你将需要过滤dev.to域,以避免看到其他所有应用程序发送的请求。
然后你可以点击请求序列,并分享到你的Mac上作进一步分析(不需要在你的移动设备上分析请求)。
现在我们要在Charles桌面程序上打开请求会话,玩玩请求。双击你的下载文件夹中的Charles会话文件(以.chlsj结尾)。
像上一部分一样,你可以用这个请求会话做完全相同的分析。
有趣的请求是/search
端点上的请求。这是一个标准的REST API。
似乎没有任何认证/cookies或特定的头文件需要获得成功的响应。你可以通过以下方式验证这一点。
- 用CMD+N创建一个新的会话
- 在dev.to上进行过滤,就像在前一部分一样
- 返回到移动开发会话
- 单击 “
Compose
“按钮并删除cookies和头文件,然后Execute
。 - 返回到桌面会话
有趣的参数是page
和per_page
计数。你可以用这些值来玩。
在将请求导出到cURL,然后将其转换为Python。
import requests params = ( ('per_page', '5'), ('page', '1'), ('sort_by', 'hotness_score'), ('sort_direction', 'desc'), ('approved', ''), ('class_name', 'Article'), ) response = requests.get('https://dev.to/search/feed_content', verify=False, params=params) print(response.text) [文中代码源自Scrapingbee]
这是我最喜欢的技术之一,可以从难以抓取的网站中提取数据。它比使用无头浏览器快得多,而且解析JSON响应比经常变化的混乱的HTML代码容易得多。
总 结
还有许多我们在本教程中没有看到的Charles功能。例如,断点和响应重写。这对于想要测试/调试单页应用程序或移动应用程序的人来说,可能非常有趣。
当你想从网站上提取数据或对移动应用程序进行逆向工程时,Charles非常方便,但它也几乎是对复杂网站进行任何形式的浏览器自动化的必经之路。
例如,用普通的浏览器开发工具,认证工作流程可能非常难以理解。一旦你开始有JWT令牌、CSRF令牌和Oauth,它就开始变得混乱,能够对一系列请求进行全文搜索并重放/修改请求是必须的。
如果你喜欢这篇文章,你可能想阅读这些。
- 用Python进行网页爬取学习在Python中爬取的不同方法
- 网页爬取而不被阻止:这将向你展示不同的技术,以避免在爬取网页时被阻止。
- 最佳网页爬取工具:这篇文章是关于最好的网页爬取工具的。
我希望你喜欢这个关于Charles和一般逆向工程的介绍,并希望你能在你喜欢的应用程序上进行实验。