HTTP STATUS CODE: 521的解决办法

一、遇到的问题

在使用Java程序访问一个PDF文件的URL时,发现无法下载该PDF文件,加上各种header也不行,然后看程序的response.getStatusLine(),发现是一个以前从未见到过的错误:HTTP/1.1 521 。
乍一看是500系列的,对面的服务崩了?但是复制url到浏览器地址栏,发现还是可以正常浏览这个PDF的。
于是清除浏览器缓存,重新访问这个URL,用大鲨鱼(wireshark)监控数据包发现这个URL刷新一次,但是被请求了两次,都被大鲨鱼监控到了,第一次的状态是521,第二次才是200。
所以弄懂第一次请时返回的数据是什么,第二次请求时浏览器的请求头以及参数是什么,然后用程序模拟浏览器的行为,就可以用代码获得数据了。
大鲨鱼监控的截图如下
可以看No.那一列,第7行是第一次请求URL的数据包,第14行是第一次请求回应的数据包。第16行是第二次请求该URL(http://ip/doc/.../abc.pdf),第82行就是第二次回应的数据包。

二、分析请求响应报文

下图是第一次请求以及回应的HTTP Stream,红色部分是请求内容,蓝紫色(就叫蓝色吧)部分是响应内容。
请求没有什么特别的,简单的get方法,请求头也没什么陌生的。
但是响应内容就有意思的多了。这个响应分两部分,一个是响应头,发现Set-Cookie,一个是响应体,从script标签开始到一大串…结束,有用的部分自然是script标签里面的东西。
第一次的请求内容和响应内容
复制这个内容到IDEA,格式化之后发现了非常反人类的代码,图片如下:
格式化之后的脚本代码
可以看出这个是定义了三个变量x,y,z还有一个函数 f然后循环。。。然后。。。总之很头疼。不过谷歌浏览器可以帮助我们查看这段脚本到底是干什么的。
创建一个html文件,把第一次请求时回应的内容复制到文件里即可。把eval函数换成console.log,用浏览器打开html文件即可查看脚本输出。
将eval替换成console.log
输出结果如下:
又是一段脚本代码
发现又是一段js代码,如法炮制,再一次用idea格式化这个新的脚本代码。结果如下:
依旧抽象但可以分析的结果
发现这段代码的大意是设置一个名称为__jsl_clearance的cookie,它的值是:
1543922503.114|0|+{一段代码的结果}+;Expires=Tue, 04-Dec-18 12:21:43 GMT;Path=/;
此时先不着急看这段代码的输出结果,先看大鲨鱼(wireshark)监控到的第二次请求情况:
第二次请求pdf的URL时,请求和响应内容
这此请求很容易理解,请求时有cookie,响应的结果也确实是pdf文件,长度为61805 字节,通过浏览器下载PDF文件,查看文件属性也确实是61805字节。
所以如果能够获得完整的cookie,那么通过get方法请求pdf的URL,即可用代码下载这个PDF文件。
这次请求的cookie有两个键值对,一个是__jsluid,它的值通过第一次请求时的响应头: set-cookie获取,另一个是__jsl_clearance,它只需运行那段神秘的脚本获得对应的值,再加上前缀:1543922503.114|0|即可。
大鲨鱼已经给出那段脚本运行的结果:9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D。
所以完整的cookie为:
__jsluid=923e768304c90e8bd92e2113f9163ca6; __jsl_clearance=1543922503.114|0|9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D
此时再用console.log的方法查看这段脚本代码生成的结果,直接提取那段生成字符串:9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D的脚本代码,看输出即可:
脚本以及运行结果
此段脚本代码的输出与大鲨鱼捕获的内容一模一样。
至此全套的访问流程已经梳理清楚。
总结一下:
通过wireshark抓包工具监控浏览器请求URL时的数据报文,发现一次刷新请求,实际上确是请求了两次,分析两次请求时的请求头、响应头、响应体即可弄清浏览器是如何请求到数据的。
浏览器在第一次请求时,会收到响应头:set-cookie,获得关键cookie中的一个:__jsluid。
通过运行第一次请求时得到的脚本(毕竟是一个用script标签修饰的脚本,浏览器会自动运行),该脚本生成新的脚本代码,新代码为浏览器提供另一个关键cookie:__jsl_clearance,以及控制浏览器带着这两个cookie再次访问该URL,就可以获取真实的PDF文件。

三、代码实现

用到的依赖包:

<dependency>
    <groupId>com.eclipsesource.j2v8</groupId>
    <artifactId>j2v8_win32_x86_64</artifactId>
    <version>4.6.0</version>
</dependency>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

具体代码如下:

public class CrackJavaScript {
    private static final Logger lg = Logger.getLogger(CrackJavaScript.class);

    /**
     * 传入pdf文件的URL, 返回下载好的PDF文件
     *
     * @param pdfUrl        pdf文件的URL地址
     * @return              pdf文件对象
     */
    public static File getPDFFile(String pdfUrl) {
        String fileName = Utils.MD5(pdfUrl);
        File file = new File("pdfFile/" + fileName + ".pdf");
        try {
            HttpClient client = HttpClients.createDefault();
            HttpGet get = new HttpGet(pdfUrl);
            setHeader(get);
            HttpResponse response = client.execute(get);

            String __jsluid = getJsluid(response);
            String body = getResponseBodyAsString(response);
            String __jsl_clearance = getJslClearance(body);
            get = new HttpGet(pdfUrl);
            get.setHeader("cookie", __jsluid + "; " + __jsl_clearance);
            setHeader(get);
            response = client.execute(get);
            output(response, file);
        } catch (Exception e) {
            lg.error(e.getMessage(), e);
        }
        return file;
    }

    /**
     * 给HttpGet设置一些必要的header
     *
     * @param get           通过get方法访问pdf资源
     */
    private static void setHeader(HttpGet get) {
        get.setHeader("Upgrade-Insecure-Requests", "1");
        get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36");
    }

    /**
     * 将HttpResponse输出到文件, 即将pdf输入流写到硬盘.
     *
     * @param response      http响应
     * @param file          落地文件
     * @throws IOException  IO异常
     */
    private static void output(HttpResponse response, File file) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(getResponseBodyAsBytes(response));
        fileOutputStream.flush();
        fileOutputStream.close();
    }

    /**
     * 通过破解动态JavaScript脚本,
     * 获取cookie名为 __jsl_clearance的值
     *
     * @param body          相应内容(一般为第一次请求获取到的动态js字符串)
     * @return              cookie名为 __jsl_clearance的值
     */
    private static String getJslClearance(String body) {
        //V8:谷歌开源的运行JavaScript脚本的库. 参数:globalAlias=window, 表示window为全局别名,
        // 告诉V8在运行JavaScript代码时, 不要从代码里找window的定义.
        V8 runtime = V8.createV8Runtime("window");
        //将第一次请求pdf资源时获取到的字符串提取成V8可执行的JavaScript代码
        body = body.trim()
                .replace("<script>", "")
                .replace("</script>", "")
                .replace("eval(y.replace(/\\b\\w+\\b/g, function(y){return x[f(y,z)-1]||(\"_\"+y)}))",
                        "y.replace(/\\b\\w+\\b/g, function(y){return x[f(y,z)-1]||(\"_\"+y)})");
        //用V8执行该段代码获取新的动态JavaScript脚本
        String result = runtime.executeStringScript(body);

        //获取 jsl_clearance 的第一段, 格式形如: 1543915851.312|0|
        String startStr = "document.cookie='";
        int i1 = result.indexOf(startStr) + startStr.length();
        int i2 = result.indexOf("|0|");
        String  cookie1 = result.substring(i1, i2 + 3);
        /*
        获取 jsl_clearance 的第二段,格式形如: DW2jqgJO5Bo45yYRKLlFbnqQuD0%3D。
        主要原理是: 新的动态JavaScript脚本是为浏览器设置cookie, 且cookie名为__jsl_clearance
        其中第一段值(格式形如:1543915851.312|0|)已经明文写好, 用字符串处理方法即可获取.
        第二段则是一段JavaScript函数, 需要有V8运行返回,
        该函数代码需要通过一些字符串定位, 提取出来, 交给V8运行.
         */
        startStr = "|0|'+(function(){";
        int i3 = result.indexOf(startStr) + startStr.length();
        int i4 = result.indexOf("})()+';Expires");
        String code = result.substring(i3, i4).replace(";return", ";");
        String cookie2 = runtime.executeStringScript(code);

        /*
        拼接两段字符串, 返回jsl_clearance的完整的值.
        格式形如: 1543915851.312|0|DW2jqgJO5Bo45yYRKLlFbnqQuD0%3D
        */
        return cookie1 + cookie2;
    }

    /**
     * 将HTTP响应体转换为字符串返回
     *
     * @param response      HTTP响应
     * @return              响应体的字符串形式
     * @throws IOException  IO异常
     */
    private static String getResponseBodyAsString(HttpResponse response) throws IOException {
        return IOUtils.readStreamAsString(response.getEntity().getContent(), "UTF-8");
    }

    /**
     * 将HTTP响应体转换为byte数组返回
     *
     * @param response      HTTP响应
     * @return              响应体的byte数组形式
     * @throws IOException  IO异常
     */
    private static byte[] getResponseBodyAsBytes(HttpResponse response) throws IOException {
        return IOUtils.readStreamAsByteArray(response.getEntity().getContent());
    }

    /**
     * 通过响应头的set-cookie
     * 获取cookie名称为__jsluid的值
     * @param response      HttpResponse
     * @return              __jsluid的值
     */
    private static String getJsluid(HttpResponse response) {
        Header header = response.getFirstHeader("set-cookie");
        String[] split = header.getValue().split(";");
        for (String s : split) {
            if (s.contains("__jsluid")) {
                return s.trim();
            }
        }
        return "";
    }
}

参考文章:
[1]: http://blog.51cto.com/12925223/2309945
[2]: https://github.com/jhao104/memory-notes/blob/master/Python/Python爬虫—破解JS加密的Cookie.md

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值