自定義小米路由器管理頁面


背景

我的路由器 7x24 開機無密碼,當耳機受影響時我會暫時關閉無線網。2015 年 11 月我的路由器壞了,朋友給我推薦小米 mini 路由器。這人自己有個小米路由器,經常不能正常啟動(那個燈是桔黃色的,不會變藍),我問他為什么還推薦小米,他說便宜功能多、不好用隨時換。我本來就沒有認定的路由器,所以就聽他的沒買 300 - 1 的極路由,買了個 130 - 1 的小米 mini。

設置好之后,用瀏覽器訪問 192.168.31.1,輸入密碼登錄管理頁面,第一感覺就是慢。

想關閉無線網

  • 常用設置,這會顯示 Wi-Fi 設置標簽 > 點擊相應 ssid 旁邊的“關閉”單選按鈕 > 保存 > 確認重啟

想啟用 mac 白名單/黑名單

  • 常用設置 > 安全中心 > 等待“無線訪問控制”出現 > 點擊它旁邊的開關 > 一條一條添加設備(若選擇“從在線列表添加”則無法修改設備名字) > 保存並生效

登錄之后顯示的頁面是 路由狀態 > (你的路由器名字),這里可以查看連接到路由器上的設備,界面類似下面

圖很直觀,文字幾乎沒用,一堆 android-

路由狀態 > 終端設備 頁面可以禁用設備訪問 wan,界面類似下面

這里顯示的名字較長,但看不到各位的流量狀況,也就無法確定該禁止誰訪問 wan。所以我想做一個列表頁面,顯示設備的完整名字和流量並控制它能否訪問 wan

工具

小米路由器 mini

google chrome 45

正文

我打算就把它顯示在 路由狀態 > (你的路由器名字),這是登錄后進入的頁面,下面稱其為首頁。

要修改首頁,可以修改保存在服務器上的文件,也可以在頁面加載之后運行一段腳本。我選擇后者因為它在瀏覽器中就能完成,缺點是每次打開首頁都要手動執行腳本。

本文不使用 fiddler 等反向代理。

snippets

chrome 的 f12 工具的 source 標簽有個保存代碼段的地方,如下

你可以把一大段腳本保存為代碼段文件,右鍵該文件選擇 run,這就省得每次都往控制台粘貼並執行代碼。本文的腳本保存在代碼段文件 miwifi 中。

位置

用 chrome 訪問 192.168.31.1,輸入密碼,會進入首頁,url 類似下面

http://192.168.31.1/cgi-bin/luci/;stok=98a00862e50a7f688cf8868869a7068f/web/home#router

要往這個頁面添加一個詳細列表,首先選取一個位置放置列表,我打算把它放在“路由器信息”下面

這意味着放在路由器信息那個 dom 元素之下,用 f12 查看他的位置

因此列表將放在 div.routerinfo 下面。在控制台執行 $(".routerinfo") 發現只有 1 個元素,所以不必擔心插入多於 1 個列表。

行為

希望構造這樣一個列表,它顯示設備的完整名字、mac、流量信息,並可以控制某個設備能否訪問 wan。這就需要大約兩個 ajax,一個獲取設備列表,另一個控制 wan 訪問。

觀察

剛才打開了控制台,應該能看到控制台有一堆輸出,由 class.pie.js:1 產生。輸出每隔一定時間產生一坨,餅圖、列表會跟着刷新,點擊 f12 的 network 標簽會發現每個成功的 xhr 調用后跟一坨輸出,xhr 是

get http://192.168.31.1/cgi-bin/luci/;stok=98a00862e50a7f688cf8868869a7068f/api/misystem/status

沒有參數

這個 console.log 為我省了不少時間,不用去查找繪制函數了。接下來要修改這個繪制函數,讓其更新前面設計的列表。

點擊控制台輸出右邊的 class.pie.js:1,這會在代碼窗格打開 class.pie.js,這是個最小化后的文件不容易閱讀,點擊打開的代碼左下角的大括號格式化它,這會在代碼窗格打開 class.pie.js:formatted,光標已經定位到 console.log 處了,在那里加一個斷點。

過一會一個新的 ajax 成功了,將調用這里的 console.log,然后會中斷在這里,查看其調用堆棧發現比較長,一堆我不關心的 jquery 代碼

blackbox

如果不想在調用堆棧里看到無關的代碼比如 jquery,可以將 jquery 添加到黑盒,這樣調用堆棧就沒有那么多噪音了。

調用堆棧顯示 PieChart.prototype.update > PieChart.prototype.drawPie > process > sector,閱讀代碼后知道 PieChart.prototype.drawPie 負責更新餅圖和列表,主要工作由 process 來做。那么只要替換 PieChart.prototype.drawPie,讓它照常更新餅圖,但更新新的列表,本文的目的就達到了。

收集

開始行動之前要找幾個可用的 ajax 以獲取設備列表及控制 wan 訪問。開着 f12 的 network 標簽四處點擊,幾個小時之后找到下面兩個 ajax,路徑相對於首頁

get ../api/xqnetwork/wifi_macfilter_info?model=

model 取值 0 - 黑名單和 1 - 白名單。該參數不重要,重要的是該 ajax 一定返回一個數組 flist,里面似乎是路由重置以來所有通過 wifi 連接的設備,不知道重置路由會不會清除 flist。這一下子就改變了我的思路,本來打算只在列表中顯示連接的設備,現在打算顯示這個完整列表,然后僅更新連接的設備了。

get ../api/xqsystem/set_mac_filter?mac=&wan=

控制 mac 訪問 wan,wan 取值 0 - 關閉和 1 - 開啟

drawPie

既然前面中斷到了 sector,咱們看看調用 sector 的 drawPie 函數有什么信息可用。drawPie 用到的信息不是通過函數參數而是通過 this 傳遞的。數組 this.datas 是設備列表,每個數組元素代表一個接入設備。數組元素包含的信息少的可憐、沒有 mac,沒有 mac 也就無法使用前面找到的 ajax set_mac_filter。為了使用 set_mac_filter 要先搞明白是調用 drawPie 的函數就沒有詳細信息還是信息沒有傳進來,然后分別采取對策。

前面知道是那個定時發起的 ajax status 調用的 drawPie,在 network 標簽查看 status 發現返回值比較詳細,包含 mac

換句話說調用 drawPie 的函數有足夠的信息,只是沒有傳進來。所以接下來不僅要修改 drawPie,還要修改 drawPie 的上游函數,讓它們給 drawPie 傳入更詳細的信息,至少要包含設備 mac。順着前面的調用堆棧往上找,除去不關心的加入黑盒的函數外就 4 個函數,按離 drawPie 從近到遠分別是

  • PieChart.prototype.update
  • 匿名函數,該函數響應消息 chart:pie_update
  • $.pub,該函數發布包括 chart:pie_update 的一些自定義消息
  • 匿名函數,ajax status 成功后進入該函數

PieChart.prototype.update

PieChart.prototype.update = function(datas, count) {
    this.datas = datas;
    this.count = count;
    this.loaddone();
    this.getTotal();
    this.drawPie();
    this.drawCount()
}

只是把傳入的 datas 加到 this 上,和 drawPie 相比沒有什么有用的信息,前面知道 datas 信息太少,還得往上找

匿名函數 $.sub("chart:pie_update", function(evt, data){ ... });

這個函數信息很全,data 是 ajax status 返回的 json 對象的 dev 屬性的原始內容,問題是如何修改它?

通過閱讀 $.sub 的源代碼

(function ($) {
    var o = $({});
    $.sub = function () { o.on.apply(o, arguments); };
    $.unsub = function () { o.off.apply(o, arguments); };
    $.pub = function () { o.trigger.apply(o, arguments); };
}(jQuery));

知道 \(.sub 通過 `\)({}).on把監聽函數保存到了 sub、unsub、pub 共同捕獲的局部變量 $({}) 中,外部無法訪問這個局部變量也就無法替換它保存的監聽函數;另一方面,調用$.sub時傳入的是匿名函數,外部取不到這個函數就沒法用 $.unsub 取消監聽(然后用$.sub` 掛接修改后的監聽函數)。所以從代碼目前編寫的方式看,無法修改這個匿名函數的行為,還得往上找

$.pub

這個代碼其實是庫代碼,但既然它直接寫在首頁的 html 里,我們可以視情況修改它。

前面知道匿名函數 \(.sub("chart:pie_update", function(evt, data){ ... }); 的 data 參數信息很全,而該函數由 `\).pub調用,所以 $.pub 的信息也很全。當$.pub發送事件 chart:pie_update 時會調用所有用 $.sub 監聽該事件的函數。查看源代碼發現只有一個函數處理 chart:pie_update 事件,就是上面那個匿名函數。所以可以修改$.pub` 讓它遇到 chart:pie_update 時直接調用基於上面的匿名函數修改的函數,遇到其它事件照常發送。

var oldPub = $.pub;

$.pub = newPub;

function newPub(type, arg) {
    if (type == "chart:pie_update") {
        // 基本上復制上面那匿名函數的內容
    } else
        oldPub.apply(null, arguments);
}

代碼段 miwifi 概述

定義一個全局變量 wifi,它里面有兩個屬性 get 和 post,分別代表 ajax get 和 ajax post,每個屬性又有一堆方法用來發起具體的 ajax;然后運行一個匿名函數繪制前面設計的列表,具體工作是

  • 用前面收集到的 ajax 填充 wifi.get 和 wifi.post
  • 保存 $.pub
  • 使用 wifi.get.wifi_macfilter_info 獲取所有通過 wifi 連接到路由器的設備列表,成功時執行另一個匿名函數

這另一個匿名函數的具體工作是

  • 在 div.routerinfo 下面添加一個 div 作為列表的容器
  • 往 document.head 追加一個 <style> 用於設置前面 div 的樣式
  • 修改 PieChart.prototype.drawPie 和 $.pub,PieChart.prototype.drawPie = newDrawPie; $.pub = newPub;
  • 把從 ajax wifi_macfilter_info 得到的 flist 的所有條目逐一添加到列表
  • 監聽列表條目的點擊事件,點擊時翻轉該條目的 wan 訪問能力

newPub 的大體工作前面已經說了,newDrawPie 要做兩件事情

  • 像以前一樣繪制餅圖
  • 修改 $.pub 之后現在的 datas 里面已經包含了設備的 mac,通過 mac 查找列表中相應的條目,給找到的條目添加流量信息。從 ajax wifi_macfilter_info 生成的列表僅包含通過 wifi 連接的設備,newDrawPie 是 ajax status 成功后調用的,該調用還包含通過網線連接的設備和一個叫 other 的流量微不足道的設備的合計,需要加以處理。

效果

修改間距,添加連接時長

代碼

本文代碼編輯於 microsoft visual studio 2015

//
// 假設當前 url 是
// http://192.168.31.1/cgi-bin/luci/;stok=bcdeb10c02009edccb478e16585b4775/web/home
//
// get
// ../api/misystem/status                   路由狀態 - 繪制餅圖時
// ../api/misystem/devicelist               路由狀態 - 查看終端設備時
// ../api/misystem/qos_info                 高級設置 > qos 智能限速 > 設備列表
// ../api/xqnetwork/wifi_macfilter_info     常用設置 > 安全中心 > 無線訪問控制
//  model - 0,默認值,黑名單列表;1,白名單列表
// ../api/xqsystem/reboot                   重啟路由器
//  client - web
// ../api/xqsystem/set_mac_filter           禁止指定的 mac 訪問 wan
//  mac - encodeURIComponent(mac)
//  wan - 0,關閉 wan;1 - 開啟 wan
//
// post
// ../api/xqnetwork/set_wifi
//  wifiIndex - 1,2.4G Wi-Fi;2,5G Wi-Fi;3,訪客 Wi-Fi
//  on - 0,關;1,開
//  ssid - ssid,比如 wangzimei
//  pwd - 該 ssid 的密碼
//  encryption - none,不使用密碼
//  channel - 0,自動
//  bandwidth - 0
//  hidden - 0
//  txpwr - max
//
// http://192.168.31.1/cn/device_list_samsung.png
//
// wifi.get.wifi_macfilter_info(0, o => { o.flist.forEach(o => { console.log(o.name, o); }); });

var wifi;

!function () {
    var oldPub = $.pub,
        template =
            '<div data-mac="{$mac}" class="{$wan}" style="background-image: url({$icon});">' +
            "    <div>{$name}</div>" +
            "    <div>{$mac}</div>" +
            "    <div class=online></div>" +
            "    <div class=download-percentage></div>" +
            "    <div class=traffic><div class=up></div><div class=down></div></div>" +
            "</div>",
        lastPicked = $([]),
        deviceList;

    wifi = {
        get: {
            devicelist: function (f) { $.getJSON("../api/misystem/devicelist", typeof f == "function" ? f : logJson); },
            qos_info: function (f) { $.getJSON("../api/misystem/qos_info", typeof f == "function" ? f : logJson); },
            reboot: function () { $.getJSON("../api/xqsystem/reboot"); },
            status: function (f) { $.getJSON("../api/misystem/status", typeof f == "function" ? f : logJson); },
            wifi_macfilter_info: function (model, f) { $.getJSON("../api/xqnetwork/wifi_macfilter_info", { model: model }, typeof f == "function" ? f : logJson); }
        },
        post: {
            set_wifi: function (on, i, ssid) {
                $.post("../api/xqnetwork/set_wifi", {
                    on: on ? 1 : 0,
                    wifiIndex: i || 1,
                    ssid: ssid || "wangzimei",
                    encryption: "none"
                });
            }
        }
    };

    wifi.get.wifi_macfilter_info(0, function (o) {
        var flist = o.flist,
            s = "",
            i, len;

        PieChart.prototype.drawPie = newDrawPie;
        $.pub = newPub;

        $("#piecharttable").html("");
        $(".new-device-list-css").length || $(document.head).append(
            "<style class=new-device-list-css>" +
            "    .new-device-list .busy { background-color: salmon; }\n" +
            "    .new-device-list .download-percentage { width: 5em; }\n" +
            "    .new-device-list .no-wan { text-decoration: line-through; }\n" +
            "    .new-device-list .online { width: 10em; }\n" +
            "    .new-device-list .traffic { width: 15em; }\n" +
            "    .new-device-list .traffic > div { line-height: 40px; }\n" +
            "    .new-device-list > div { background-position: left center; background-repeat: no-repeat; background-size: 60px; display: flex; line-height: 80px; text-align: right; transition: background-color 0.5s ease; }\n" +
            "    .new-device-list > div > :nth-child(1) { width: 20em; }\n" +
            "    .new-device-list > div > :nth-child(2) { width: 10em; }\n" +
            "</style>");

        $(".new-device-list").remove();
        $("<div class=new-device-list></div>").insertAfter(".routerinfo");
        deviceList = $(".new-device-list");

        for (i = 0, len = flist.length; i < len; ++i)
            s += StringH.tmpl(template, {
                mac: flist[i].mac,
                wan: flist[i].authority.wan ? "" : "no-wan",
                name: escapeHtml(flist[i].name),
                icon: flist[i].company.icon ? "/cn/" + flist[i].company.icon : "/img/device_list_unknow.png"
            });

        deviceList.html(s);
        deviceList.on("click", "[data-mac]", onClickDevice);
    });

    function logJson(json) { console.log(json); }

    function onClickDevice() {
        var t = $(this);

        if (!t.hasClass("busy")) {
            t.addClass("busy");

            $.getJSON("../api/xqsystem/set_mac_filter", { mac: t.attr("data-mac"), wan: t.hasClass("no-wan") ? 1 : 0 })
                .done(function (json) { json.code || t.toggleClass("no-wan"); })
                .always(function () { t.removeClass("busy"); });
        }
    }

    function newPub(type, arg) {
        var dev, colorMap, total, i, len, value, bf;

        if (type == "chart:pie_update") {
            dev = arg.devStatistics;
            colorMap = ["#33cc33", "#2673bf", "#ffaa00", "#ff6600", "#d96cb5", "#00baff", "#ff4060", "#00d990", "#d96cca", "#ff4400"];
            total = 0;

            for (i = 0, len = dev.length; i < len; ++i) {
                value = parseInt(dev[i].download, 10);
                dev[i].value = value;
                dev[i].value2 = parseInt(dev[i].downspeed, 10);
                dev[i].color = colorMap[i];
                total += value;
            }

            bf = byteFormat(total, 10, true);
            $.pieChart.update(dev, {
                value: bf[0],
                label: bf[1]
            });
        } else
            oldPub.apply(null, arguments);
    }

    function newDrawPie() {
        var r = this.r - 2,
            angle = 0,
            start = 0,
            currentPicked = [],
            i, len, angleplus, pie, o, div;

        lastPicked.css("color", "");
        lastPicked.find(".online").text("");
        lastPicked.find(".download-percentage").text("");
        lastPicked.find(".up").text("");
        lastPicked.find(".down").text("");

        for (i = 0, len = this.datas.length; i < len; ++i) {
            o = this.datas[i];
            o.color || (o.color = Raphael.hsb(start, 0.8, 0.9)),
            angleplus = 360 * o.value / this.total,
            pie = sector(this.paper, this.rad, this.cx, this.cy, r, angle, angle + angleplus, {
                fill: o.color,
                stroke: this.stroke,
                "stroke-width": 0
            });

            angle += angleplus;
            start += 0.1;
            pie.id = "pie_" + i;
            this.chart.push(pie);

            div = deviceList.find('[data-mac="' + o.mac + '"]');

            if (!div.length) {
                o.icon = "/img/device_list_unknow.png";
                div = $(StringH.tmpl(template, o));
                div.children(":first-child").text(o.devname);
            }

            div.css("color", o.color);
            div.find(".online").text($.secondToDate(o.online));
            div.find(".download-percentage").text((this.total ? (o.value / this.total * 100).toFixed(1) : 0) + "%");
            div.find(".up").text(byteFormat(o.upspeed) + "/S - " + byteFormat(o.upload));
            div.find(".down").text(byteFormat(o.downspeed) + "/S - " + byteFormat(o.download));
            currentPicked.push(div[0]);
        }

        lastPicked = $(currentPicked);
        deviceList.prepend(lastPicked);
    }

    function sector(paper, rad, cx, cy, r, startAngle, endAngle, params) {
        var x1 = cx + r * Math.cos(-startAngle * rad),
            x2 = cx + r * Math.cos(-endAngle * rad),
            y1 = cy + r * Math.sin(-startAngle * rad),
            y2 = cy + r * Math.sin(-endAngle * rad);

        return endAngle - startAngle == 360 ?
            paper.circle(cx, cy, r).attr(params) :
            paper.path(["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(endAngle - startAngle > 180), 0, x2, y2, "z"]).attr(params);
    }

    // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
    function escapeHtml(s) {
        var entityMap = {
            "&": "&amp;",
            "<": "&lt;",
            ">": "&gt;",
            '"': "&quot;",
            "'": "&#39;",
            "/": "&#x2F;"
        };

        return s.replace(/[&<>"'\/]/g, function (str) {
            return entityMap[str];
        });
    }
}();

// 關於路由器密碼
// 路由器密碼和 mac 地址差不多,都是客戶向路由器提交一個字符串,路由器查表以確定是否接受連接,密碼可以泄露,mac 也可以偽造。
//
// 關於小米路由器
// 優點:
//  便宜
//  可以禁止設備訪問外網
//  獲取設備型號
// 缺點:
//  ajax 速度很慢
//  動不動就要重啟路由器,重啟路由器尤其慢
//  很難找在哪設置某項內容
//  mac 地址只能用冒號分隔
//  不知道在哪看日志,只看到有個上傳日志的按鈕
//  即使從白名單刪除了設備,設備列表里面仍然是以前自己指定的名字,無法查看設備原來的名字
//  /img/device_list_err.png、/img/device_list_unknow.png 和其它設備圖標路徑不一樣,並缺少下列圖片
//      /cn/device_list_google.png
//      /cn/device_list_vivo.png
//      /cn/device_list_windows.png
// 不明覺厲的功能:
//  我 1 個朋友 1 年多前來我家,此后再沒來過。當時我把他蘋果手機的 mac 加入以前的路由器白名單了。前段時間換路由,
//  把白名單拷貝到這個小米中,當時沒注意,過兩天發現這個白名單條目關聯的圖標是蘋果。小米能根據 mac 判斷手機型號?
//
// 360免費wifi的原理是什么?有無竊取手機記錄的wifi數據?
// http://www.zhihu.com/question/27065773/answer/35111715
//
// 如何看待小米路由進行 404 網頁劫持?
// http://www.zhihu.com/question/30358197
//
// 小米路由器劫持用戶瀏覽器事件回顧
// http://drops.wooyun.org/tips/6820
//
// 小米路由器先劫持 http 錯誤碼, 現在又在部分網站添加小尾巴, 什么節奏?
// https://www.v2ex.com/t/199701

使用

打開 192.168.31.1 登錄 > 按 f12 > 轉到 sources 標簽 > 選 snippets > 右鍵 new > 輸入文件名比如 miwifi > 把上面的代碼粘貼到打開的文件內 > 保存 > 右鍵文件名 > run


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM