效果展示

guruguru.tk网址
效果图

项目介绍

guruguru.tk 是一个英文句式搜索引擎项目,数据来源于ludwig.guru

由于原网站ludwig.guru限制每个终端每天只能查询5次,无法满足学生的求知需求,同时 由于原网站服务器于国外,响应速度不够,不能够很快的得到结果
guruguru.tk采用了代理+数据库缓存的方式,你的每一次搜索,都会将原服务器最新结果存在guruguru自己的数据库里,下一次再搜索,就会以非常快的速度搜索出结果,解决了原网站 响应过慢的问题,同时guruguru.tk也不限制搜索次数,任意终端每天可以无限次搜索,但请不要滥用!

编写过程

抓包分析网页

简要分析

使用firefox打开原网页ludwig.guru

QQ截图20200316173330.jpg

发现搜索几次过后弹出了限制搜索次数的提示

2.jpg

清除浏览器cookie后重新试一下

3.jpg

发现又可以重新搜索了

4.jpg

从这里就可以大致推测网站原理,如果是第一次访问网页,分配一个cookie到本地,然后每次请求的时候会发送某个身份信息,服务器根据身份信息验证搜索次数。所以清空cookie后,服务器会自动重新分配一个新cookie,然后就可以继续搜索了

具体抓包

既然知道了原理,现在开始抓包分析请求的api地址

打开火狐开发者工具,来到网络栏

然后随便搜索一个单词,来到结果界面,查看火狐抓包情况

5.jpg

发现xhr这一栏有一个请求,看起来很像api地址,来看看响应

6.jpg

可以大致确定这就是api地址了,我们可以得到以下信息

请求网址: https://ludwig.guru/api/search?q=test

请求方法: GET

同时,作为抓包的通常套路,我们还应该关注一下他的请求头,请求头如下

Host: ludwig.guru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: https://ludwig.guru/s/test
Version: 2.4.4-101-ga38e93a.web
Authorization: jtt4fgces7siffck1hdl7qq1351e4iaipoipdntfpor1vcfrin5k
Connection: keep-alive
Cookie: [太长,省略]

注意到Authorization这个头有点不寻常,普通的http请求不会自动添加上这个参数,猜测这就是服务器用来验证请求合法性的参数,我们这里用Postman模拟发包来验证一下我们的猜想

首先打开Postman正常的建立一个请求,把firefox捕捉到的请求头都放进去

7.jpg

尝试发包,发现正常返回

8.jpg

去除掉Authorization,再次发包,验证我们之前的猜想

9.jpg

发现,服务器返回错误代码401,表示没有获得授权,猜想正确

10.jpg

所以我们已经完成了抓包的基本工作,由于每个Authorization只能搜索固定次数,搜索次数消耗完就不能用了,我们要实现不限制次数的后端自然不能固定一个Authorization,而需要动态获取最新可用的Authorization,那么现在问题就是如何获得这个Authorization呢?

解决获取Authorization的问题

我们之前分析已经说明过,根据网页的性质,去除cookies过后可以重复搜索,我们猜测有可能服务器在每次新的访客到来时,会给他分配一个最新的cookies,保证他能够查询指定次数,而这个Authorization我们猜测也和cookies密切相关

服务器如何知道一个请求是来自一个新的用户还是来自一个已经查询过几次的用户呢?很简单,如果这个用户不是第一次来这个网站,那么他的请求包里必然会被浏览器加上之前的cookies。简单来说,服务器只需要判断请求头里面是否包含cookies,如果不包含,则是新用户,如果包含,则是老用户

所以现在我们要做的,就是让服务器以为我们是新用户,然后每次都傻乎乎的给我们发一个新的有效的cookies,这样不是就能实现无限搜索次数了吗?

说干就干,我们猜测服务器分配新cookies的页面就是首页,于是我们使用Postman模拟请求主页,同时,禁止Postman发送cookies

11.jpg

我们发现,服务器的响应头里,有Set-Cookie属性,这意味着服务器尝试设置本地的cookies,我们也正式要这个东西

12.jpg

我们拿到这个cookies,看看他和Authorization有什么关联

_ljwt=eyJtZXNzYWdlIjoiV2VsY29tZSB0byBMdWR3aWciLCJ0b2tlbiI6InRuMXEyNG0xOXFsN3FhOWg1c3ByNWc1cXU1czJhNjBsdm1jOThrNWc0Y20zZ2NldG43Z3YiLCJuIjozLCJmaXJzdE5hbWUiOiJndWVzdC01NzE2MDk2NyIsImlkIjo1NzE2MDk2Nywicm9sZSI6Imd1ZXN0Iiwic3Vic2NyaXB0aW9uIjoiZnJlZSIsInN1YnNjcmlwdGlvbkFjdGl2YXRpb24iOjE1ODQzNTI0OTU3NTEsInN1YnNjcmlwdGlvbkV4cGlyYXRpb24iOjE1ODQ0Mzg4OTU3NTEsImZpcnN0TG9naW4iOnRydWV9; Max-Age=31536000; Domain=ludwig.guru; Path=/; Expires=Tue, 16 Mar 2021 09:54:55 GMT

注意到_ljwt这个cookies键,他的键值特别长,可是乍一看似乎是一堆乱码,但是稍作分析不难发现,这其实只是简单的base64编码,我们解码看看结果:

13.jpg

"{\"message\":\"Welcome to Ludwig\",\"token\":\"tn1q24m19ql7qa9h5spr5g5qu5s2a60lvmc98k5g4cm3gcetn7gv\",\"n\":3,\"firstName\":\"guest-57160967\",\"id\":57160967,\"role\":\"guest\",\"subscription\":\"free\",\"subscriptionActivation\":1584352495751,\"subscriptionExpiration\":1584438895751,\"firstLogin\":true}"

惊喜的发现,这居然是json格式的数据,并且看看token这一项的后面,居然就是长度和我们之前的Authorization相同的字符串,我们猜测这就是我们要得Authorization,把这里解码的token放到Postman的Authorization里面去试一下,能否正常请求

14.jpg

15.jpg

成功运行,得到响应结果

至此,所有抓包工作已基本完成,我们现在已经知道我们的大概工作流程了,先去请求一个可用的崭新的Authorization,再用这个Authorization去请求服务器的接口获得结果,为我们所用,接下来就是Coding Time!!!

Coding Time

技术栈决策

我平时用的比较熟悉的是PHP,那就用html5前端,PHP+mysql做后端吧!

接口编写

最主要的一个接口就是搜索接口,我们在搜索接口里要做的,就是从原服务器请求返回数据,然后加入数据库里面以便以后查询更加迅速,同时把数据返回出去。如果搜索内容已经在数据库里,就直接从数据库返回就可以了

php发送GET,POST请求什么的都挺方便的,在这里我封装了一个Utils.php用于请求崭新的Authorization,代码如下:

<?php
    function cut($begin,$end,$str){
        $b = mb_strpos($str,$begin) + mb_strlen($begin);
        $e = mb_strpos($str,$end) - $b;
        return mb_substr($str,$b,$e);
    }
    function requireCookie(){
        $targetUrl = "https://ludwig.guru";
        $oCurl = curl_init();
        $header = array(
            "Accept:" . "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Accept-Encoding:" . "gzip, deflate, br",
            "Accept-Language:" . "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
            "Host:" . "ludwig.guru",
            "User-Agent:" . "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"
        );
        curl_setopt($oCurl, CURLOPT_URL, $targetUrl);
        curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($oCurl, CURLOPT_HTTPHEADER,$header);
        curl_setopt($oCurl, CURLOPT_HEADER, true);
        curl_setopt($oCurl, CURLOPT_NOBODY, true);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($oCurl, CURLOPT_POST, false);
        $sContent = curl_exec($oCurl);
        $headerSize = curl_getinfo($oCurl, CURLINFO_HEADER_SIZE);
        curl_close($oCurl);
        return cut("_ljwt=",";",substr($sContent, 0, $headerSize));
    }
    function requireAuthorization(){
        $cookie = requireCookie();
        $res = base64_decode($cookie);
        return cut("\"token\":\"","\",\"n\"",$res);
    }
?>

这里直接偷了个懒,没有解析json,直接用的取文本中间的办法

然后是数据库辅助类,用于数据库的增删查改

<?php
    const DB_IP = "127.0.0.1:3306";
    const DB_USER = "root";
    const DB_PASSWORD = "root";
    const DB_DBNAME = "guruguru.tk";
    
    
    $conn = new mysqli(DB_IP, DB_USER, DB_PASSWORD, DB_DBNAME);
    if(!$conn){
        die("Cannot connect to the database!");
    }
    function isSearchInHistory($param){
        global $conn;
        $sql = "select response from history where param='{$param}'";
        $res = $conn->query($sql);
        $row = mysqli_fetch_assoc($res);
        if ($row > 0){
            return true;
        }else{
            return false;
        }
    }
    function getSearchHistory($param){
        global $conn;
        $sql = "select response from history where param='{$param}'";
        $res = $conn->query($sql);
        $row = mysqli_fetch_assoc($res);
        if($row > 0){
            return $row['response'];
        }else{
            return "";
        }
    }
    function addSearchHistory($param,$response){
        global $conn;
        $response1 = addslashes($response);
        $sql = "INSERT INTO `history` (`param`, `response`) VALUES ('{$param}', '{$response1}')";
        $res = $conn->query($sql);
    }
?>

另外,数据库的结构是这样的:

16.jpg

由于response可能很长,所以使用了可以存储超长字符串的text类型

最后,就是最核心的搜索请求接口了,代码如下:

<?php
    require_once('./Utils/common.php');
    require_once('./database.php');
    const ALLOW_REF = "*";
    $ref = parse_url($_SERVER["HTTP_REFERER"])["host"];
    if(!isset($_POST['content'])){
        var_dump($_POST);
        http_response_code(401);
        exit();
    }
    if(ALLOW_REF != "*" && $ref != ALLOW_REF){
        http_response_code(401);
        exit();
    }
    $param = urlencode($_POST['content']);
    if(strlen($_POST['content']) > 100){
        echo "Too long for searching!";
        http_response_code(401);
        exit();
    }
    if(isSearchInHistory($param)){
        echo getSearchHistory($param);
        die();
    }
    $targetUrl = "https://ludwig.guru/api/search?q=" . $param;
    $header = array(
        "Accept:" . "application/json, text/plain, */*",
        "Accept-Language:" . "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Host:" . "ludwig.guru",
        "User-Agent:" . "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0",
        "Version:" . "2.4.4-101-ga38e93a.web",
        "Authorization:" . requireAuthorization()
    );
    $oCurl = curl_init();
    curl_setopt($oCurl, CURLOPT_ACCEPT_ENCODING, "gzip,deflate");
    curl_setopt($oCurl, CURLOPT_URL, $targetUrl);
    curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($oCurl, CURLOPT_HTTPHEADER,$header);
    curl_setopt($oCurl, CURLOPT_HEADER, false);
    curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($oCurl, CURLOPT_POST, false);
    $sContent = curl_exec($oCurl);
    curl_close($oCurl);
    echo $sContent;
    if(strlen($sContent) > 10) addSearchHistory($param,$sContent);
?>

在里面限制了一下字符串请求长度,因为原官网也限制了,为了预防位置错误,限制了长度

至此,后端代码编写完毕,前端代码也非常好编写,这里我就不放代码了,大家可以自己在guruguru.tk网址审查元素查看

补充

另外,这次我还使用了github的Action进行自动部署,每次提交代码过后,github就会自动部署到生产环境,大概1分钟就能看到官网的效果,非常方便,大家也可以试一下

17.jpg

总结

整个项目其实用到的技术并不多,也不是什么特别新奇的玩法,简单的把cookies解码一下就可以做到

不过通过自己的编程,写出了一个对自己学习有帮助的网站,那乐趣自然也是无穷的。我还把这个网站推荐给了同学使用,他们都觉得这很实用

最后,放上几张项目截图吧:

18.jpg

19.jpg

20.jpg