[手摸手教你]用油猴(篡改猴)給寶塔證書排個序

亦般發表於2024-06-15

[手摸手教你]用油猴(篡改猴)給寶塔證書排個序

喜歡用寶塔的朋友可能也有這個困擾,就是寶塔網站列表沒辦法給SSL證書過期時間這一欄排序
最近管理大量網站證書,苦於這個無法排序,沒辦法直觀檢視哪些證書已過期或者快過期,一頁頁翻的我想死啊
剛好之前用過一點油猴外掛,知道這個東西神通廣大,想實現這個功能應該還挺簡單,所以有了這個嘗試
廢話少說咱開搞 (ps.想省事的同學直接拉到最後看原始碼)

[手摸手教你]用油猴(篡改猴)給寶塔證書排個序

分析需求

可能有同學會說了,🤔不就是排個序嗎,用的著上油猴,控制檯指令碼分分鐘給你排好是
當然我剛開始也是這麼想的程式碼如下

// SSL過期排序
var tbody = document.getElementsByTagName('tbody')
var list = document.getElementsByTagName("tr")

var indexStatus = 8  // SSL證書欄位置 根據實際列表從左往右順序
console.info("%c ========================================","color:green;")
console.info("%c ========================================","color:green;")
console.info(`%c 確認SSL證書欄位置  第 ${indexStatus} 列`,"color:blue;font-weight:bold")
console.info("%c ========================================","color:green;")
var domListEnd = []  // 已過期狀態
var domListRest = [] // 剩餘狀態
var domListOther = [] // 其他狀態
for(let i=list.length-1;i>0;i--){
	const item = list[i]
	if(item&&item.childNodes.length>indexStatus){
		const status = item.childNodes[indexStatus].textContent
		if(status.match("已過期")){
			domListEnd.push(item)
		}else if(status.match(/剩餘\d+天/)){
			item.exp = status.replace(/剩餘(\d+)天/,"$1")
			domListRest.push(item)
		}else{
			domListOther.push(item)
		}
	}
}
// 剩餘天數排序
var domListRestOrder = domListRest.sort((a,b)=>{return a.exp - b.exp})

// 狀態排序
var orderList = domListEnd.concat(domListRestOrder).concat(domListOther)

// 重新顯示dom
tbody[0].innerHTML = null
orderList.forEach(function(item) {
    tbody[0].append(item)
}); 
console.info("%c !!僅限檢視列表SSL過期順序,請勿點選詳情操作,詳情資料不對!!","color:red;font-weight:bold")

控制檯執行以上程式碼,即可對SSL證書進行排序

!注意:點選詳情頁面是錯誤的,並非所點選的站點
如果只需要看順序,我再送你一段程式碼

var pageSize = 500; // 分頁數量
var pageDom =  document.createElement("option")
    pageDom.setAttribute("value",pageSize);
    pageDom.innerHTML= `${pageSize}條/頁`;
document.getElementsByClassName("page_select_number")[0].appendChild(pageDom)

執行後會新增一個分頁項
image

經過以上嘗試,修改頁面dom確實能排好頁面,如果你只需要檢視順序,配合分頁程式碼全部展示後再排序,完全沒有問題
但是如果你想點進去操作詳情,發現牛頭不對馬嘴,根本不是你點選的那個網站了,這個目前沒找到解決方案,所以最終還是請出大殺器油猴吧

油猴指令碼

油猴的安裝在此就不多說了,有需要的看另一篇文章油猴外掛安裝(Chrome)

進入寶塔網站頁面,點選油猴外掛新增新指令碼,新的指令碼會自動設定好當前頁面URL的匹配設定
image

在編輯指令碼頁面完成以下步驟

  1. 給指令碼起一個響亮的名字
  2. 設定指令碼生效的瀏覽器URL路徑,頁面新建的指令碼會自動設定好(也可設定正則匹配)
  3. 寫上測試程式碼

image

ctrl+s儲存指令碼

回到寶塔頁面,把油猴外掛固定在右上角,可以看見出現紅點表示有一條規則開啟,指令碼下拉選還可以快捷操作
image

正常開啟指令碼後,重新整理即可看見控制檯輸出了
image

經過嘗試,發現寶塔使用的是jquery的ajax請求,簡單攔截一下

const originFetch = window.$.ajax; 

window.$.ajax = (...arg) => {
		console.log('ajax ', arg); 
		return originFetch(...arg);
}

可在控制檯找到攔截的輸出
image

根據官方api https://www.bt.cn/data/api-doc.pdf 和網頁請求記錄可知

網站列表的請求地址url就是這裡的url: /data?action=getData
請求引數就是這裡的 data 物件 { table: 'sites', limit: '10',... }

可能api與實際使用有出入,api中的表名是連結引數,實際上是data引數中

官方api截圖
image

繼續迭代,寫個正則實現精準攔截介面

    const RegExp_SiteList = /\/data\?action=getData/g

    const originFetch = window.$.ajax;

    window.$.ajax = (...arg) => {
        const url = arg[0].url;
        const params = arg[0].data
        // ==================
        // 請求攔截
        // ==================
        // 請求網站列表
        if (url.match(RegExp_SiteList) && params.table=="sites") {
            console.log("請求網站列表")
            console.log('ajax ', arg);
            // 改變分頁引數
            arg[0].data.limit = 5;
        }

        return originFetch(...arg);
    }

這樣控制檯我們就只看見一條記錄了

注意:有可能出現偶爾控制檯不輸出的情況,重新整理頁面多試一下,機智的同學可能看出程式碼的問題了,這個問題我們稍後最佳化

image

繼續迭代,最終我們需要攔截ajax返回資料改變資料順序,也就是這裡的success方法
檢視返回資料後,發現ssl是一個物件
image

根據不同狀態資料分析,需要對資料稍微處理一下再排序


const RegExp_SiteList = /\/data\?action=getData/g

const originFetch = window.$.ajax;

window.$.ajax = (...arg) => {
	const url = arg[0].url;
	const params = arg[0].data
	// ==================
	// 請求攔截
	// ==================
	// 請求網站列表
	if (url.match(RegExp_SiteList) && params.table=="sites") {
		console.log("請求網站列表")
		console.log('ajax ', arg);
		// 改變分頁引數
		arg[0].data.limit = 5;

		// ==================
		// 返回資料攔截
		// ==================
		const originSuccess = arg[0].success;
		arg[0].success = (...res)=>{
			console.log(res)
			res[0].data.map(item=>{
				// 處理一下未部署的情況,排到最後
				if(item.ssl==-1){
					item.sslEndtime = 9999
				}else{
					// 其他按過期時間順序,最先過期的排前面
					item.sslEndtime = item.ssl.endtime
				}
			})

			res[0].data.sort((a,b)=>{
				return a.sslEndtime - b.sslEndtime;
			})
			return originSuccess(...res)
		}
	}

	return originFetch(...arg);
}

以後需要給證書過期排序的時候只需要開一下開關就可以了,不用了就關掉,舒服~😎
image

最佳化

指令碼的注入時間是透過@runat引數控制的

@run-at document-idle
指令碼將在 DOMContentLoaded 事件派發後注入。如果沒有給出 @run-at 標籤,則這是預設值。
@run-at document-start
指令碼將盡快注入。
@run-at document-body
如果 body 元素存在,則將注入指令碼。
@run-at document-end
指令碼將在 DOMContentLoaded 事件派發時或之後注入。
@run-at content-menu
如果在瀏覽器上下文選單中單擊指令碼,則將注入指令碼(僅限基於 Chrome 的桌面瀏覽器)。

因為預設的注入時間,導致有可能介面已經呼叫,但是指令碼還沒載入的情況,所以不能立即攔截到我們想要的介面
這裡我們需要將指令碼設定為儘快注入,並且做一個迴圈判斷,直到jq載入完

// ==UserScript==
...
// @run-at       document-start
...

// 迴圈判斷
function waitForJs() {
	// jq載入完判斷
	if (typeof window.$ !== 'undefined') {
		// 注入指令碼主邏輯
	} else {
		// 如果條件不滿足,則在10ms後再次嘗試
		setTimeout(waitForJs, 10);
	}
}

指令碼原始碼

// ==UserScript==
// @name         寶塔助手
// @namespace    http://tampermonkey.net/
// @version      2024-06-07
// @description  try to take over the world!
// @author       ban
// @match        [你的寶塔地址]
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
	'use strict';

	function waitForJs() {
		// jq載入完判斷
		if (typeof window.$ !== 'undefined') {
			// 注入指令碼主邏輯
			injectJqAjax();
		} else {
			// 如果條件不滿足,則在10ms後再次嘗試
			setTimeout(waitForJs, 10);
		}
	}
	waitForJs();


	const RegExp_SiteList = /\/data\?action=getData/gi

	function injectJqAjax(){
		console.warn("[injectJqAjax] start")
		const originFetch = window.$.ajax;

		window.$.ajax = (...arg) => {
			const url = arg[0].url;
			const params = arg[0].data
			// ==================
			// 請求攔截
			// ==================
			// 請求網站列表
			if (url.match(RegExp_SiteList) && params.table=="sites") {
				// 改變分頁引數
				arg[0].data.limit = 500;

				// ==================
				// 返回資料攔截
				// ==================
				const originSuccess = arg[0].success;
				arg[0].success = (...res)=>{
					res[0].data.map(item=>{
						// 處理一下未部署的情況,排到最後
						if(item.ssl==-1){
							item.sslEndtime = 9999
						}else{
							// 其他按過期時間順序,最先過期的排前面
							item.sslEndtime = item.ssl.endtime
						}
					})

					res[0].data.sort((a,b)=>{
						return a.sslEndtime - b.sslEndtime;
					})
					return originSuccess(...res)
				}
			}

			return originFetch(...arg);
		}
	}

 })();

相關文章