前言
现在很多服务的正常运行都依赖某个中心服务器,如果服务器升级或出现故障或者公司跑路,付费用户将无法继续正常使用。emby也是如此,程序要访问mb3admin.com确认你是付费用户才能使用更多的功能,不管你的个人网络还是他们服务器出问题,都将不能使用付费功能。
网络上有一些解决方案,但大都需要搭建一个nginx伪装站点,生成ssl证书,emby服务器上添加证书并且修改hosts文件,客户端也需要修改hosts或者重定向mb3admin.com到伪站……我认为这样破解太过复杂了,用别人的伪站又担心不稳定。
所以,就有了今天的尝试,修改emby程序,让其在不设置伪站的情况下也能使用最基本的是付费功能。
破解过程
需要用到下面两个工具:
js-beautify
dnSpy
emby-server在Linux系统中安装于/opt/emby-server/,把此文件夹复制到hack-emby目录,并在此目录执行下面的脚本,美化javascript代码,以方便阅读。
#!/bin/bash
for f in $(find emby-server/ -name "*.js")
do
echo "$f"
js-beautify "$f" > tmp
mv tmp "$f"
done
在浏览器打开emby,在Emby Premiere页面随便输入一个key按保存后提示Emby Premiere key is missing or invalid.
搜索定义并调用这个字符串的位置,发现仅在 system/dashboard-ui/embypremiere/embypremiere.html
中有调用这个字符串,同目录有个 embypremiere.js
,根据 MediaBrowser.Model.dll
中的某些函数返回值,执行相关的判断。修改 MediaBrowser.Model.dll
中的 get_IsMBSupporter
和 get_SupporterKey
的返回值后不再提示这个错误,不过也不像付费用户一样显示。
还需要修改 emby-server/system/dashboard-ui/embypremiere/embypremiere.js
中的load(page)函数,去除访问 mb3admin.com
相关的代码,并且硬编码返回给付费用户的json数据才能正常显示。
function load(page) {
var apiClient;
loading.show(), (apiClient = ApiClient).getJSON(apiClient.getUrl("Plugins/SecurityInfo")).then(function(info) {
var key, postData;
page.querySelector("#txtSupporterKey").value = info.SupporterKey || "", page.querySelector("#txtSupporterKey").classList.remove("invalidEntry"), page.querySelector(".notSupporter").classList.add("hide")
var statusInfo = {"deviceStatus":"0","planType":"Lifetime","subscriptions":{}};
var statusLine, indicator = page.querySelector("#status-indicator .listItemIcon"),
extendedPlans = page.querySelector("#extended-plans");
switch (extendedPlans.innerHTML = globalize.translate("MessagePremiereExtendedPlans", '<a is="emby-linkbutton" class="button-link" href="https://emby.media/premiere-ext.html" target="_blank">', "</a>"), statusInfo.deviceStatus) {
default:
statusLine = globalize.translate("MessagePremiereStatusGood", statusInfo.planType), indicator.classList.remove("expiredBackground"), indicator.classList.remove("nearExpiredBackground"), indicator.innerHTML = "", extendedPlans.classList.add("hide")
}
page.querySelector("#premiere-status").innerHTML = statusLine;
var sub, subsElement = page.querySelector("#premiere-subs");
statusInfo.subscriptions && 0 < statusInfo.subscriptions.length ? (page.querySelector("#premiere-subs-content").innerHTML = (subs = statusInfo.subscriptions, key = info.SupporterKey, subs.map(function(item) {
var itemHtml = "",
makeLink = item.autoRenew && "Stripe" === item.store,
tagName = makeLink ? "button" : "div";
return itemHtml += ("button" == tagName ? '<button type="button"' : "<div") + ' class="listItem listItem-button listItem-noborder' + (makeLink ? " lnkSubscription" : "") + '" data-feature="' + item.feature + '" data-key="' + key + '">', itemHtml += '<i class="listItemIcon md-icon">dvr</i>', itemHtml += '<div class="listItemBody two-line">', itemHtml += '<div class="listItemBodyText">', itemHtml += globalize.translate("ListItemPremiereSub", item.planType, item.expDate, item.store), itemHtml += "</div>", itemHtml += '<div class="listItemBodyText listItemBodyText-secondary">', itemHtml += globalize.translate("Stripe" === item.store ? item.autoRenew ? "LabelClickToCancel" : "LabelAlreadyCancelled" : "LabelCancelInfo", item.store), itemHtml += "</div>", itemHtml += "</div>", itemHtml += "</" + tagName + ">"
})), (sub = page.querySelector(".lnkSubscription")) && sub.addEventListener("click", cancelSub), subsElement.classList.remove("hide")) : subsElement.classList.add("hide"), page.querySelector(".isSupporter").classList.remove("hide")
//var subs, key
}), loading.hide()
}
这里并不是最重要的,也许恰恰是最不重要的,因为我修改相关的代码后,Dashboard页面并没有显示徽章,不能使用付费主题,不能看live tv,右上角的升级会员按钮没有消失。这个程序以前是开源的,后来闭源后越来越不好破解了!(不过javascript和C#跟开源没啥区别)
显示徽章
Dashboard中徽章位于emby-server/system/dashboard-ui/css/images/supporter/supporterbadge.png,脚本system/dashboard-ui/dashboard/dashboard.js中的renderSupporterIcon函数使用了这个图标,调用这个函数的代码段为:
function() {
var apiClient = window.ApiClient;
return apiClient ? connectionManager.getRegistrationInfo("themes", apiClient, {
viewOnly: !0
}).then(function(result) {
return {
IsMBSupporter: !0
}
}, function() {
return {
IsMBSupporter: !1
}
}) : Promise.reject()
}().then(function(pluginSecurityInfo) {
DashboardPage.renderSupporterIcon(page, pluginSecurityInfo);
var html, supporterPromotionElem = page.querySelector(".supporterPromotion"),
isSupporter = pluginSecurityInfo.IsMBSupporter;
只要把IsMBSupporter: !1改为 IsMBSupporter: !0
就可以显示徽章了。修改 emby-server/system/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js
的getRegistrationInfo应该会更好,因为多处代码都调用了这个函数!
key: "getRegistrationInfo",
value: function(feature, apiClient, options) {
var params = {
serverId: apiClient.serverId(),
deviceId: this.deviceId(),
deviceName: this.deviceName(),
appName: this.appName(),
appVersion: this.appVersion(),
embyUserName: ""
};
(options = options || {}).viewOnly && (params.viewOnly = options.viewOnly);
var cacheKey = getCacheKey(feature, apiClient, options),
regInfo = JSON.parse(this.appStorage.getItem(cacheKey) || "{}"),
timeSinceLastValidation = Date.now() - (regInfo.lastValidDate || 0);
if (timeSinceLastValidation <= 864e5) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();
var regCacheValid = timeSinceLastValidation <= 864e5 * (regInfo.cacheExpirationDays || 7);
params.embyUserName = apiClient.getCurrentUserName();
var currentUserId = apiClient.getCurrentUserId();
if (currentUserId && "81f53802ea0247ad80618f55d9b4ec3c" === currentUserId.toLowerCase() && "21585256623b4beeb26d5d3b09dec0ac" === params.serverId.toLowerCase()) return Promise.reject();
var appStorage = this.appStorage,
getRegPromise = ajax({
url: "https://mb3admin.com/admin/service/registration/validateDevice?" + paramsToString(params),
type: "POST",
dataType: "json"
}).then(function(response) {
return appStorage.setItem(cacheKey, JSON.stringify({
lastValidDate: Date.now(),
deviceId: params.deviceId,
cacheExpirationDays: response.cacheExpirationDays
})), Promise.resolve()
}, function(response) {
var status = (response || {}).status;
return console.log("getRegistrationInfo response: " + status), 403 === status ? Promise.reject("overlimit") : status && status < 500 ? Promise.reject() : function(err) {
if (console.log("getRegistrationInfo failed: " + err), regCacheValid) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();
throw err
}(response)
});
return regCacheValid ? (console.log("getRegistrationInfo returning cached info"), Promise.resolve()) : getRegPromise
}
把 if (timeSinceLastValidation <= 864e5) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();
中的 timeSinceLastValidation <= 864e5
替换为true,确保始终返回本地缓存中的数据,而不再需要找服务器验证。
不过程序可能为了防止这个js文件被修改,在 emby-server/system/dashboard-ui/app.js
中有这么一段代码:
define("connectionManagerFactory", [], getDynamicImport("./bower_components/emby-apiclient/connectionmanager.js"))
运行时从emby-server/system/Emby.Web.dll中动态导入这个js文件,而不是使用 emby-server/system/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js
,使用dnSpy的Hex Editor修改这几个字节就可以了。
破解会员功能
上面两个并没那么重要,上传、下载、电视直播和dvr这类功能才是更加实用的,这些都跟validateFeature有关系。
同样,这个函数不仅存在于system/dashboard-ui/modules/registrationservices/registrationservices.js,在Emby.Web.dll中也有备份,我们要修改后者才能生效。
{
validateFeature: function(feature, options) {
return options = options || {}, console.log("validateFeature: " + feature), iapManager.isUnlockedByDefault(feature, options).then(function() {
return showPeriodicMessageIfNeeded(feature)
}, function() {
var unlockableFeatureCacheKey = "featurepurchased-" + feature;
if ("1" === appSettings.get(unlockableFeatureCacheKey)) return showPeriodicMessageIfNeeded(feature);
var unlockableProduct = iapManager.getProductInfo(feature);
if (unlockableProduct) {
var unlockableCacheKey = "productpurchased-" + unlockableProduct.id;
if (unlockableProduct.owned) return appSettings.set(unlockableFeatureCacheKey, "1"), appSettings.set(unlockableCacheKey, "1"), showPeriodicMessageIfNeeded(feature);
if ("1" === appSettings.get(unlockableCacheKey)) return showPeriodicMessageIfNeeded(feature)
}
var unlockableProductInfo = unlockableProduct ? {
enableAppUnlock: !0,
id: unlockableProduct.id,
price: unlockableProduct.price,
feature: feature
} : null;
return iapManager.getSubscriptionOptions().then(function(subscriptionOptions) {
if (0 < subscriptionOptions.filter(function(p) {
return p.owned
}).length) return Promise.resolve();
var registrationOptions = {
viewOnly: options.viewOnly
};
return connectionManager.getRegistrationInfo(iapManager.getAdminFeatureName(feature), connectionManager.currentApiClient(), registrationOptions).catch(function(errorResult) {
return !1 === options.showDialog ? Promise.reject() : ("overlimit" === errorResult && (alertPromise = alertText("Your Emby Premiere device limit has been exceeded. Please check with the owner of your Emby Server and have them contact Emby support at support@emby.media if necessary.").catch(function() {
return Promise.resolve()
})), (alertPromise = alertPromise || Promise.resolve()).then(function() {
var dialogOptions = {
title: globalize.translate("HeaderUnlockFeature"),
feature: feature
};
return currentValidatingFeature = feature, showInAppPurchaseInfo(subscriptionOptions, unlockableProductInfo, dialogOptions)
}));
var alertPromise
})
})
})
},
showPremiereInfo: showPremiereInfo
}
修改这个函数的返回值,并保存。替换掉系统中相关的文件,刷新浏览器缓存,就可以使用会员的大部分功能了。
上图是破解前,即使正版程序,在局域网无法访问mb3admin.com时,可能也无法使用付费功能。下图为破解后,emby-server不再访问mb3admin.com而是直接返回缓存中/硬编码在dll和js中的数据。
致敬作者:
评论 (0)