Android webkit对于网络传入的数据流解码分析

前面分析了,我们在接收数据的时候,会去创建Dom树的根节点为Document -> HTMLDocument,和render tree的根节点 RenderView 以及解析器DocumentParser。

但是具体到一个网页的时候,应该怎么去解析呢?
一个网络上的数据流到HTMLTokenizer之间会做了一些什么事情呢?
准备接着进行下面的分析。

还是先来一个堆栈说明问题:

#0  HTMLDocumentParser (this=0x2a2e8798, document=0x2a283a18, reportErrors=false) at external/webkit/Source/WebCore/html/parser/HTMLDocumentParser.cpp:77
#1  0x48f22a3a in WebCore::HTMLDocumentParser::create (this=0x2a283a18) at external/webkit/Source/WebCore/html/parser/HTMLDocumentParser.h:61
#2  WebCore::HTMLDocument::createParser (this=0x2a283a18) at external/webkit/Source/WebCore/html/HTMLDocument.cpp:281
#3  0x48eeb7ec in WebCore::Document::implicitOpen (this=0x2a283a18) at external/webkit/Source/WebCore/dom/Document.cpp:1987
#4  0x48f389e8 in WebCore::DocumentWriter::begin (this=0x2a3b16fc, url=<value optimized out>, dispatch=<value optimized out>, origin=0x0) at external/webkit/Source/WebCore/loader/DocumentWriter.cpp:149
#5  0x48d47f82 in WebCore::FrameLoader::receivedFirstData (this=0x2a146f20) at external/webkit/Source/WebCore/loader/FrameLoader.cpp:609
#6  0x48f3869c in WebCore::DocumentWriter::setEncoding (this=0x2a3b16fc, name=..., userChosen=<value optimized out>) at external/webkit/Source/WebCore/loader/DocumentWriter.cpp:243
#7  0x48f36c30 in WebCore::DocumentLoader::commitData (this=0x2a3b16a8, 


这个堆栈应该是很熟悉了。
DocumentLoader::commitData -> DocumentWriter::setEncoding -> if(receivedFirstData) -> begin -> implicatiOpen -> createParser -> HTMLDocumentParser::create -> HTMLDocumentParser的构造函数.

而且在上一个文章的分析中我们知道了在创建完Parser后,是使用 parser->appendBytes(this, str, len, flush)去向parser模块传输数据。
下面这个堆栈来看一下是怎么来解析数据的:

 

#0  HTMLTokenizer (this=0x2a4527b8, usePreHTML5ParserQuirks=false) at external/webkit/Source/WebCore/html/parser/HTMLTokenizer.cpp:106
#1  0x48f27f28 in WebCore::HTMLTokenizer::create (this=0x2a463b08, document=<value optimized out>) at external/webkit/Source/WebCore/html/parser/HTMLTokenizer.h:123
#2  HTMLPreloadScanner (this=0x2a463b08, document=<value optimized out>) at external/webkit/Source/WebCore/html/parser/HTMLPreloadScanner.cpp:155
#3  0x48f26b0e in WebCore::HTMLDocumentParser::pumpTokenizer (this=0x2a2e8798, mode=WebCore::HTMLDocumentParser::AllowYield) at external/webkit/Source/WebCore/html/parser/HTMLDocumentParser.cpp:293
#4  0x48f26d52 in WebCore::HTMLDocumentParser::append (this=0x2a2e8798, source=...) at external/webkit/Source/WebCore/html/parser/HTMLDocumentParser.cpp:367
#5  WebCore::HTMLDocumentParser::append (this=0x2a2e8798, source=...) at external/webkit/Source/WebCore/html/parser/HTMLDocumentParser.cpp:337
#6  0x48fbe2c4 in WebCore::DecodedDataDocumentParser::appendBytes (this=0x2a2e8798, writer=<value optimized out>, 
    data=0x2a413ef0 "b/gp/?tab=wm\"><div class=\"gbzi gbsi\" style=\"background-position:-32px -50px\"></div><span class=gbzn>Gmail</span></a><a onclick=gbar.logger.il(1,{t:25}); id=gb_25 class=\"gbza\" href=\"https://drive.googl"..., length=12398, shouldFlush=false) at external/webkit/Source/WebCore/dom/DecodedDataDocumentParser.cpp:54
#7  0x48f384ea in WebCore::DocumentWriter::addData (this=0x2a3b16fc, 
    str=0x2a413ef0 "b/gp/?tab=wm\"><div class=\"gbzi gbsi\" style=\"background-position:-32px -50px\"></div><span class=gbzn>Gmail</span></a><a onclick=gbar.logger.il(1,{t:25}); id=gb_25 class=\"gbza\" href=\"https://drive.googl"..., len=12398, flush=<value optimized out>) at external/webkit/Source/WebCore/loader/DocumentWriter.cpp:207


具体的流程为:
WebCore::DocumentWriter::addData -> WebCore::DecodedDataDocumentParser::appendBytes -> WebCore::HTMLDocumentParser::append -> WebCore::HTMLDocumentParser::append -> WebCore::HTMLDocumentParser::pumpTokenizer -> HTMLPreloadScanner -> WebCore::HTMLTokenizer::create -> HTMLTokenizer

 

void DocumentWriter::addData(const char* str, int len, bool flush)
{
    if (len == -1)
        len = strlen(str);


    DocumentParser* parser = m_frame->document()->parser();
    if (parser)
        parser->appendBytes(this, str, len, flush);
}


当发现有parser解释器创建的时候,就往parser里面去传输数据。
这里使用的是DocumentParser的appendBytes,但是appendBytes这个函数在DocumentParser里声明的是一个虚函数,在这个里面并没有实现。
具体的实现是在DecodedDataDocumentParser这个类中。 

 

void DecodedDataDocumentParser::appendBytes(DocumentWriter* writer , const char* data, int length, bool shouldFlush)
{
    if (!length && !shouldFlush)
        return;


    TextResourceDecoder* decoder = writer->createDecoderIfNeeded();
    String decoded = decoder->decode(data, length);
    if (shouldFlush)
        decoded += decoder->flush();
    if (decoded.isEmpty())
        return;


    writer->reportDataReceived();


    append(decoded);
}


在这个里面,首先会去创建一个decoder。创建的过程是会在DocumentWriter里面进行的创建。
在第一次打开这个网页的时候,m_decoder这个值肯定是为0的。这样的话,在createDecoderIfNeeded里面,就肯定会进行decode的创建了
比如decode是text的?还是img?当然,我们现在关心的是HTML的网页,所以这边只研究HTML的情况就可以了。
在确定完decode创建以后,就会进行下面的操作。
String decoded = decoder->decode(data, length);
docoded是一个TextResourceDecoder的对象,所以看一下在TextResourceDecoder.cpp里面的实现。
这边首先会去判断checkForHeadCharset,这个函数的作用是什么呢?
它是用来检查HTML头信息中是否有编码的信息,一般HTML的页面中如果指定了编码信息,就会放在<head>的标签中。

decode完了之后的值究竟是什么呢? 我们来看个例子:

data=0x2a5e52f8 "<!DOCTYPE HTML>\n<html>\n    <!--head-->\n    <head>\n        <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n        <meta http-equiv=\"Cache-Control\" content=\"no-cache\" />\n        <"..., length=1303, shouldFlush=false) at external/webkit/Source/WebCore/dom/DecodedDataDocumentParser.cpp:46


在这个例子里面,我们可以看到charset=utf-8,那么就会通过TextResourceDecoder::setEncoding把监测到的编码格式给TextResourceDecoder。
如何设置这个编码格式呢?还是先贴了个堆栈信息:

 

#0  WebCore::TextResourceDecoder::checkForMetaCharset (this=0x2a9c26b0, 
    data=0x2aa36d68 "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" co"..., length=2293) at external/webkit/Source/WebCore/loader/TextResourceDecoder.cpp:579
#1  0x48d532ac in WebCore::TextResourceDecoder::checkForHeadCharset (this=0x2a9c26b0, 
    data=0x2aa36d68 "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" co"..., len=2293, movedDataToBuffer=<value optimized out>) at external/webkit/Source/WebCore/loader/TextResourceDecoder.cpp:575
#2  0x48d535b2 in WebCore::TextResourceDecoder::decode (this=0x2a9c26b0, 
    data=0x2aa36d68 "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" co"..., len=2293) at external/webkit/Source/WebCore/loader/TextResourceDecoder.cpp:638
#3  0x48fbe25c in WebCore::DecodedDataDocumentParser::appendBytes (this=0x2a41dc78, writer=0x2a46dc54, 
    data=0x2aa36d68 "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" co"..., length=2293, shouldFlush=false) at external/webkit/Source/WebCore/dom/DecodedDataDocumentParser.cpp:46
#4  0x48f384ea in WebCore::DocumentWriter::addData (this=0x2a46dc54, 
    str=0x2aa36d68 "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" co"..., len=2293, flush=<value optimized out>) at external/webkit/Source/WebCore/loader/DocumentWriter.cpp:207


具体的调用为: WebCore::DocumentWriter::addData -> WebCore::DecodedDataDocumentParser::appendBytes -> WebCore::TextResourceDecoder::decode  -> WebCore::TextResourceDecoder::checkForHeadCharset  -> WebCore::TextResourceDecoder::checkForMetaCharset
在TextResourceDecoder.cpp的TextResourceDecoder::setEncoding函数中,我们看到了编码信息的设置:m_encoding = encoding;
gdb去查看了一下m_encoding的值为
(gdb) p m_encoding
$22 = {m_name = 0x491851ca "UTF-8", m_backslashAsCurrencySymbol = 92}
这样的话,就完成了对编码格式的设置。-> TextCodecUTF8.cpp

重新回到DecodedDataDocumentParser::appendBytes
但是docode为:$16 = {m_impl = {m_ptr = 0x2a41f8c8}}

这个时候shouldFlush是false,decoded并不是null所以这两个判断都不会进入。
data经过解码后,已经变成了docode的字符串。

writer->reportDataReceived()是什么作用呢?顾名思义,是报告有新的数据被接收。
向谁报告呢?m_frame->document()->recalcStyle(Node::Force); 通过这个我们可以看到是调用的document的recalctyle而参数为Force。
在Document::recalcStyle(StyleChange change)这个函数中,我们着重关注Force的情况:

 

    if (change == Force) {
        // style selector may set this again during recalc
        m_hasNodesWithPlaceholderStyle = false;


        RefPtr<RenderStyle> documentStyle = CSSStyleSelector::styleForDocument(this);
        StyleChange ch = diff(documentStyle.get(), renderer()->style());
        if (renderer() && ch != NoChange)
            renderer()->setStyle(documentStyle.release());
    }


当第一次载入网页的时候,会进入到这个函数的判断中。这边就会根据当前网页的信息去确定CSSStyleSelector的选择.
CSSStyleSelector这个类的作用是什么呢?


CSSStyleSelector是一个重要的类,在CSSStyleSelector的构建中,会缺省载入一些style,这些style是以代码的形式存在UserAgentStyleSheets.h和UserAgentStyleSheetsData.cpp中的,按数据是以整数的形式保存的,不太容易看出来数据的具体意义。

当如果现在的style和载入的网页的style是不一样的时候,就会重新设置render的sytle。

再回到DecodedDataDocumentParser.cpp中,DecodedDataDocumentParser::appendBytes接下来就会调用append(decoded);
而在DocumentParser的创建过程中,针对HTML的网页,最终子类的HTMLDocumentParser会对其进行实现。
所以,在这个例子中我们走到的是 void HTMLDocumentParser::append(const SegmentedString& source)中进行具体的处理。

在HTMLDocumentParser中有成员HTMLInputStream m_input;此处,把参数传入的String数据追加到HTMLDocumentParser::m_input,这样HTMLDocumentParser中已经保存了解码后的字符串了。
至次,解码过程已经完毕,我们又处于HTMLDocumentParser中,并且已经保存了解码后的输入数据。
具体操作为:m_input.appendToEnd(source);

在HTMLDocumentParser::append里面,pumpTokenizerIfPossible(AllowYield)是需要我们额外关注的。
这个的实现其实非常简单,只是对pumpTokenizer(mode);起了一层封装的效果。

 

void HTMLDocumentParser::pumpTokenizerIfPossible(SynchronousMode mode)
{
    if (isStopped() || m_treeBuilder->isPaused())
        return;


    // Once a resume is scheduled, HTMLParserScheduler controls when we next pump.
    if (isScheduledForResume()) {
        ASSERT(mode == AllowYield);
        return;
    }


    pumpTokenizer(mode);
}


去判断在进行词法解析之前,HTMLTreeBuilder是否完成暂停,DocumentParser是否已经停止,或者是否需要重新启动一次HTMLParserScheduler
如果没有的话就进行pumpTokenizer的操作。

pumpTokenizer(mode)的函数原型为:
void HTMLDocumentParser::pumpTokenizer(SynchronousMode mode)

在这个里面首先声明了一个对象:PumpSession session(m_pumpSessionNestingLevel);
然后就会进入到一个while的循环,在这个循环里面是需要去判断needsYield的值的。所以我们要看一下这个值是在哪边被改变的?

 

 

 

void checkForYieldBeforeToken(PumpSession& session)
{
    if (session.processedTokens > m_parserChunkSize) {
        // currentTime() can be expensive.  By delaying, we avoided calling
        // currentTime() when constructing non-yielding PumpSessions.
        if (!session.startTime)
            session.startTime = currentTime();


        session.processedTokens = 0;
        double elapsedTime = currentTime() - session.startTime;
        if (elapsedTime > m_parserTimeLimit)
            session.needsYield = true;
    }
    ++session.processedTokens;
}

 



判断当前的时间如果为0的话,就给session的startTime赋予一个初始的事件,这个时间就等于系统当前的时间。
然后elapsedTime就等于当前的时间减去上一次到达这个函数时的时间,然后当这个时间如果大于一个解析限制的时间的话,就会去对needsYield设置为true。
设置这个是一个超时的判断,如果是进入到了超时的情况下,则在其它的函数中放弃当前的这个操作。

由于在实际的解析中,这个解析的时间非常的快,所以这个值一直为false,并且进入到了循环中,

在循环里面:m_tokenizer->nextToken(m_input.current(), m_token)是一个非常重要的操作。

在这个时候,我们会发现,HTMLDocumentParser::m_input中已经存有解码后的字符串,HTMLTokenizer的对象m_tokenizer在HTMLDocumentParser的构造函数中一并被创建出来。这样的话,解析器已经创建完毕,等待被解析的字符串也已经到位,就等待解析的开始了。

具体的解析过程我们在下面接着分析。

总结一下:
从DocumentWriter这个桥梁将数据传送到HTMLParser模块的时候,我们首先会根据head和meta标签去确定当前网页的编码格式,并且会对输入的数据进行编码格式上的转换,转换后的数据将会被传输到HTMLParser模块去进行下一步的解析。
在解析的过程中,对于处理的时间也是有要求的。系统规定的时间内如果没有解析完成的话,我们就会将这个解析的过程进行抛弃。
在第一次载入一个网页的时候,会根据CSSStyle的方式对当前网页的css布局进行一个初步的选择。
当如果现在的style和载入的网页的style是不一样的时候,就会重新设置render的sytle。
在HTMLDocumentParser.cpp中的HTMLDocumentParser::pumpTokenizer运行的时候,这个时候,经过解码的数据流已经就位,解析器m_tokenizer也随着HTMLDocumentParser的构造函数被创建了出来。

前提条件都已经准备就绪,接下来就去分析HTMLTokenizer怎么去对数据流进行解析。

 

 

 

 

zaixian_cas_login.py脚本的原始脚本内容是: #!/usr/bin/python3 # coding=utf-8 import io import sys import time import requests import json import re import base64 from urllib.parse import urlparse, urljoin, quote import urllib3 import gzip import zlib import brotli import chardet from typing import Optional, Tuple, Dict # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') SUC_RES = { 'resCode': 200, 'resTime': 0, 'keyword': 'SUCCESS', 'message': [] } FAIL_RES = { 'resCode': 500, 'resTime': 0, 'keyword': 'FAILED', 'message': [] } # 封装解码 class HttpResponseProcessor: def __init__(self, url: str, headers: Optional[Dict] = None): """ 初始化响应处理器 :param url: 请求的URL :param headers: 请求头,默认为None """ self.url = url self.headers = headers or {} self.response = None self.raw_content = None self.text_content = None self.encoding = None self.status_code = None def fetch_response(self): """ 发送HTTP请求并获取响应 :return: None """ try: self.response = requests.get( url=self.url, headers=self.headers, allow_redirects=False, # 禁用自动重定向 stream=True, # 流模式获取原始响应 verify=False ) self.status_code = self.response.status_code self.raw_content = self.response.content except Exception as e: raise Exception(f"请求失败: {str(e)}") def print_response_headers(self): """ 打印响应头信息 :return: None """ if not self.response: raise Exception("尚未获取响应,请先调用 fetch_response()") def decode_content(self) -> str: """ 尝试解码内容为文本 :return: 解码后的文本内容 """ if not self.raw_content: raise Exception("尚未获取原始内容,请先调用 fetch_response()") try: # 检测内容编码 result = chardet.detect(self.raw_content) encoding_detected = result.get('encoding') if result else None # 尝试解码 if encoding_detected: try: self.text_content = self.raw_content.decode(encoding_detected) self.encoding = encoding_detected return self.text_content except UnicodeDecodeError: # 如果检测到的编码解码失败,则尝试其他编码 pass # 尝试常见编码 for encoding in ['utf-8', 'gbk', 'gb2312', 'latin1']: try: self.text_content = self.raw_content.decode(encoding) self.encoding = encoding break except: continue else: # 如果都无法解码,则使用替换错误字符的方式解码 try: self.text_content = self.raw_content.decode('utf-8', errors='replace') self.encoding = 'utf-8' except: self.text_content = "无法解码内容" self.encoding = None return self.text_content except Exception as e: # 将内容保存到文件以便分析 with open('response.bin', 'wb') as f: f.write(self.raw_content) raise Exception(f"内容解码失败: {str(e)}") def process_response(self) -> Tuple[int, Optional[str], Optional[str]]: """ 完整处理响应的便捷方法 :return: (status_code, text_content, encoding) """ self.fetch_response() self.print_response_headers() text = self.decode_content() return self.status_code, text, self.encoding def print_err_result(e): FAIL_RES['error'] = e exit(1) def make_request(url, params=None, data=None, method='get', session=None): try: start = time.time() req_func = session.get if session else requests.get if method.lower() == 'post': req_func = session.post if session else requests.post headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'application/json,text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'X-Requested-With': 'XMLHttpRequest', 'accept-encoding': 'gzip, deflate, br,zstd' } response = req_func( url, params=params, data=data, verify=False, headers=headers ) res_time = (time.time() - start) * 1000 if response.status_code in [200, 302]: SUC_RES['resTime'] = int(res_time) SUC_RES['message'].append(f"请求 {url} 成功") return response else: FAIL_RES['error'] = f"请求失败,状态码: {response.status_code}, 响应内容: {response.text}, 头信息:{session.headers if session else None}" FAIL_RES['message'].append(f"请求 {url} 失败") return None except Exception as e: print_err_result(f"请求过程中发生错误: {str(e)}") return None def cas_login(username, password) -> Tuple[Optional[str], Optional[dict]]: # 使用会话保持cookies session = requests.Session() token = None try: # 第一步:获取lt令牌 params1 = { 'service': 'https://www.fifedu.com/iplat/ssoservice', 'get-lt': 'true', 'n': str(int(time.time() * 1000)), 'callback': 'jsonpcallback', '_': str(int(time.time() * 1000)) } url1 = "https://cycore.fifedu.com/cas-server/login" response1 = make_request(url1, params=params1, session=session) if not response1: return None, {} # 1. 检查响应是否以jsonpcallback开头 if not response1.text.startswith('jsonpcallback'): raise ValueError("响应格式不符合预期,不是JSONP格式") # 2. 提取括号内的JSON部分 json_str = response1.text[len('jsonpcallback('):-2] # 去掉首尾的jsonpcallback(和); # 3. 将字符串解析为字典 try: data = json.loads(json_str) except json.JSONDecodeError: raise ValueError("JSON解析失败,响应内容: " + response1.text) # 4. 提取所需的值 lt = data.get('lt', '') execution = data.get('execution', '') if not lt or not execution: raise ValueError("响应中缺少lt或execution字段") # 第二步:提交登录表单 # 注意:密码是base64编码的,但这里我们直接使用传入的密码(原始代码中密码是base64编码的,所以这里我们直接使用) # 实际上,在登录请求中,密码应该是明文还是编码?根据观察,原始代码中密码是base64编码的,但登录表单提交的是原始密码还是编码后的? # 由于我们传入的password已经是base64编码(从get_credentials中获取的),但实际登录接口可能需要明文,所以这里需要先解码? # 但是,在原始代码中,密码是直接以base64字符串形式传入的,而登录接口是否要求base64编码?需要根据实际接口要求。 # 由于我们不清楚,所以先按照原始代码的方式,直接传入base64字符串作为密码。 data2 = { 'service': 'https://cycore.fifedu.com/iplat/ssoservice', 'callback': 'logincallback', 'isajax': 'true', 'isframe': 'true', '_eventId': 'submit', 'serviceURL': 'null', 'lt': lt, 'type': 'pwd', 'execution': execution, 'username': username, 'password': password, '_': str(int(time.time() * 1000)) } url2 = "https://cycore.fifedu.com/cas-server/login" response2 = make_request(url2, data=data2, method='post', session=session) if not response2: return None, {} # 检查登录是否成功 response_text = response2.text.strip() if response_text.startswith("logincallback"): json_str = response_text[len("logincallback("):-2] try: login_result = json.loads(json_str) except json.JSONDecodeError: raise ValueError("登录响应JSON解析失败: " + response_text) token = login_result.get("token", "") ticket = login_result.get("ticket", "") if not token or not ticket: raise ValueError("登录响应中缺少token或ticket") else: raise ValueError("登录响应格式不符合预期: " + response_text) # 第三步:ssosevice跳转 params3 = { 'callback': f'jQuery{int(time.time()*1000000)}_{int(time.time()*1000)}', 'action': 'login', '_': str(int(time.time() * 1000)) } # 更新请求头,注意:这里我们不再手动设置Cookie,而是由session自动管理 session.headers.update({ 'Referer': 'https://www.fifedu.com/iplat/fifLogin/scuai/index.html?service=https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': '*/*', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' }) url3 = "https://www.fifedu.com/iplat/ssoservice" response3 = session.get( url3, params=params3, allow_redirects=True, verify=False ) if not response3: return None, {} # 第四步:跳转到目标页面 params4 = { 'nextPage': 'https://assess.fifedu.com/testcenter/home/teacher_index', } url4 = "https://www.fifedu.com/iplat/ssoservice" # 注意:这里我们不再手动设置Cookie头,而是由session自动管理 session.headers.update({ 'Referer': 'https://www.fifedu.com/iplat/fifLogin/scuai/index.html?service=https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'priority': 'u=0, i', 'Upgrade-Insecure-Requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', }) response4 = session.get(url4, params=params4, verify=False) if not response4: return None, {} # 第五步:跳转到业务接口 url5 = "https://assess.fifedu.com/testcenter/home/getUser" session.headers.update({ 'Referer': 'https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': '*/*', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'priority': 'u=0, i', 'Cookie': f'prod-token={token}', # 这里设置token到Cookie,但注意session可能已经自动管理了,所以这一步可能是多余的,因为后面会使用这个token 'cache-control': 'no-cache', 'pragma': 'no-cache', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' }) response5 = session.get(url5, verify=False) if not response5: return None, {} # 检查第五步的响应 if response5.status_code != 200: raise ValueError(f"获取用户信息失败,状态码: {response5.status_code}") # 获取session中的cookies sess = session.cookies.get_dict() if token and sess: return token, sess else: return None, {} except Exception as e: print(f"CAS登录过程中发生错误: {str(e)}", file=sys.stderr) return None, {} def get_credentials(): username = "jffwbc1" password = "R2pjcHgxMjMhQCM=" # base64编码的密码 # 注意:这里我们不进行解码,因为登录函数中直接使用了这个base64字符串作为密码。 # 但是,根据实际接口,可能需要明文密码,那么就需要先解码: # password = base64.b64decode(password).decode('utf-8') # 但是,原始代码中直接使用base64字符串作为密码,所以我们先保持原样。 return cas_login(username, password) if __name__ == '__main__': username = "jffwbc1" password = "R2pjcHgxMjMhQCM=" token, sess = cas_login(username, password) if token and sess: print("登录成功!") print(f"Token: {token}") print(f"Session Cookies: {sess}") else: print("登录失败!") zaixian.py脚本的原始脚本内容是: #!/usr/bin/python3 # coding=utf-8 from zaixian_cas_login import HttpResponseProcessor from zaixian_cas_login import get_credentials import sys import time import requests import json import ast from urllib.parse import urlparse, urljoin SUC_RES = { 'resCode': 200, 'resTime': 0, 'keyword': 'SUCCESS', 'message': "调用成功", 'apiMessage': None } FAIL_RES = { 'resCode': 500, 'resTime': 0, 'keyword': 'FAILED', 'message': "调用失败", 'apiMessage': None } def print_err_result(e): FAIL_RES['error'] = str(e) print(json.dumps(FAIL_RES, ensure_ascii=False)) exit(1) def parse_params(params): """更健壮的参数解析函数,支持多种格式""" if not params or params.lower() == 'null': return {} # 尝试直接解析为标准JSON try: return json.loads(params) except json.JSONDecodeError: pass # 尝试解析为Python字面量(支持单引号) try: return ast.literal_eval(params) except (ValueError, SyntaxError): pass # 尝试处理类JSON格式(单引号) try: sanitized = params.replace("'", '"') return json.loads(sanitized) except json.JSONDecodeError: pass raise ValueError(f"无法解析的参数格式: {params}") def _requests(full_url, params='{}'): try: # 解析参数 pars = parse_params(params) # 获取请求参数 data = pars.get('data', {}) method = pars.get('method', 'GET').upper() expected_message = pars.get('expectedMessage', None) # 添加协议前缀 if not full_url.startswith(('http://', 'https://')): full_url = 'https://' + full_url # 验证URL格式 parsed_url = urlparse(full_url) if not parsed_url.netloc: raise ValueError("无效的URL格式,缺少域名部分") # 确保路径正确 if not parsed_url.path.startswith('/'): full_url = urljoin(full_url, '/') isSuccess = True start = time.time() response_data = None api_message = None try: # 获取认证信息 token, sess = get_credentials() if token is None or sess is None: raise ValueError("无法获取有效的token或session") # 设置请求头 headers = { 'Cookie': f'prod-token={token}', 'Content-Type': 'application/json' } # 执行请求 if method == 'POST': res = requests.post(url=full_url, json=data, headers=headers, verify=False) else: res = requests.get(url=full_url, params=data, headers=headers, verify=False) # 处理响应 processor = HttpResponseProcessor(full_url, headers=res.headers) processor.response = res processor.raw_content = res.content processor.status_code = res.status_code try: # 解码内容 text_content = processor.decode_content() # 尝试解析JSON try: response_data = json.loads(text_content) api_message = response_data.get('message', None) except json.JSONDecodeError: response_data = {'raw_response': text_content} except Exception as e: raise e except requests.exceptions.SSLError as e: raise Exception('SSL证书验证失败') from e except Exception as e: raise Exception('调用出现异常') from e # 计算耗时 res_time = (time.time() - start) * 1000 # 获取状态码 try: statusCode = response_data.get('statusCode', res.status_code) except AttributeError: statusCode = res.status_code # 判断请求结果 if res.status_code != 200: isSuccess = False FAIL_RES['message'] = f"HTTP状态码错误: {res.status_code}" FAIL_RES['apiMessage'] = api_message or getattr(response_data, 'resInfo', '') elif statusCode != 200: isSuccess = False try: res_info = response_data.get('responseBody', '') or response_data.get('resInfo', '') FAIL_RES['message'] = f"业务状态码错误: {statusCode} - {res_info}" FAIL_RES['apiMessage'] = api_message except (ValueError, AttributeError): FAIL_RES['message'] = '解析响应内容失败' # 处理预期消息验证 if expected_message is not None and api_message != expected_message: isSuccess = False FAIL_RES['message'] = f"消息验证失败: 预期 '{expected_message}', 实际 '{api_message}'" FAIL_RES['apiMessage'] = api_message # 输出结果 if isSuccess: SUC_RES['resTime'] = int(res_time) SUC_RES['apiMessage'] = api_message print(json.dumps(SUC_RES, ensure_ascii=False)) else: FAIL_RES['resTime'] = int(res_time) print(json.dumps(FAIL_RES, ensure_ascii=False)) except Exception as e: print_err_result(e) if __name__ == '__main__': args = sys.argv[1:] if len(args) < 1: print_err_result(''' 参数不足 用法: ./http_requests.py 完整URL [JSON参数] 示例: ./http_requests.py "api.example.com/endpoint" '{"data":{"key":"value"}, "method":"POST"}' 注意: JSON参数需要使用单引号包裹,内部使用双引号 ''') full_url = args[0] params = args[1] if len(args) > 1 else '{}' _requests(full_url, params)
最新发布
08-07
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值