Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
在開發(fā)領(lǐng)域,我們經(jīng)常會(huì)遇到需要?jiǎng)討B(tài)加載和執(zhí)行代碼的場景。對于Python、JavaScript、Lua等腳本語言,動(dòng)態(tài)性是它們的天性,而對于需要預(yù)先編譯的語言,如C#,動(dòng)態(tài)執(zhí)行似乎并不那么直觀。但隨著AI的普及,例如我們想在C#程序中動(dòng)態(tài)執(zhí)行AI生成的代碼段,這就要求我們能在運(yùn)行時(shí)編譯和執(zhí)行C#代碼。接下來,讓我為你介紹一個(gè)強(qiáng)大的框架——Natasha。
Natasha是一個(gè)基于Roslyn的C#動(dòng)態(tài)程序集構(gòu)建庫。它允許開發(fā)者在程序運(yùn)行時(shí)動(dòng)態(tài)地構(gòu)建域、程序集、類、結(jié)構(gòu)體、枚舉、接口和方法等。這意味著開發(fā)者可以在不停止應(yīng)用程序的情況下,為其增加新的程序集。
Natasha框架具備域管理和插件管理功能,支持域的隔離和卸載,實(shí)現(xiàn)了熱插拔。它提供了完善的錯(cuò)誤提示,自動(dòng)添加引用,并且擁有完整的數(shù)據(jù)結(jié)構(gòu)構(gòu)建模板,從而讓開發(fā)者專注于程序集腳本的編寫。更何況它還有著跨平臺的優(yōu)勢,并且對netcoreapp2.0+ / netcoreapp3.0+都兼容。
你可能會(huì)好奇,這樣一個(gè)動(dòng)態(tài)編譯庫是如何彰顯其價(jià)值的?其實(shí),動(dòng)態(tài)編譯技術(shù)是支撐如今.NET生態(tài)不可或缺的重要部分。無論是在官方還是非官方的庫中,動(dòng)態(tài)編譯的技術(shù)都扮演著“服務(wù)”的角色。其核心是MSIL,官方為我們提供了Emit技術(shù)來編寫IL代碼。但Emit的編寫和維護(hù)并不友好,給開發(fā)者帶來了諸多挑戰(zhàn)。
Roslyn的出現(xiàn)仿佛開啟了新世界的大門,它使得Emit變得透明,并允許我們直接用C#進(jìn)行動(dòng)態(tài)編譯。Natasha就是在這樣的基礎(chǔ)上發(fā)展起來的,經(jīng)過精心設(shè)計(jì)與不斷迭代,它正成為動(dòng)態(tài)編譯領(lǐng)域的佼佼者。
借助Natasha,你可以實(shí)現(xiàn)眾多有趣而實(shí)用的功能,如創(chuàng)建AOP代理類或動(dòng)態(tài)構(gòu)建Controller來實(shí)現(xiàn)動(dòng)態(tài)API,甚至在程序啟動(dòng)時(shí)與CodeFirst兼容的ORM一起使用,動(dòng)態(tài)創(chuàng)建表結(jié)構(gòu),甚至通過動(dòng)態(tài)執(zhí)行AI創(chuàng)建的代碼段,這是個(gè)非常有趣的事情!
不可否認(rèn),這些功能的實(shí)現(xiàn)需要一定的編程基礎(chǔ)。例如,下面的代碼展示了如何使用Natasha快速開始一個(gè)域,并利用其插件管理功能。
// 開始創(chuàng)建域
var domain=new NatashaDomain();
// 創(chuàng)建非主域
var domain=new NatashaDomain(key);
// 加載插件
var assembly=domain.LoadPlugin(pluginPath);
// 使用程序集比較器
domain.SetAssemblyLoadBehavior(AssemblyCompareInfomation.UseHighVersion);
// 封裝API
domain.LoadPluginWithHighDependency(PluginPath);
在智能編譯模式下,你可以使用如下代碼快速地進(jìn)行編譯,Natasha將智能地合并元數(shù)據(jù)和Using聲明,并進(jìn)行語義檢查。
AssemblyCSharpBuilder builder=new AssemblyCSharpBuilder();
var myAssembly=builder.UseRandomDomain()
.UseSmartMode()
.Add("public class A{ }")
.GetAssembly();
對于更加輕便的編譯需求,Natasha提供了簡潔編譯模式。該模式會(huì)合并當(dāng)前域的元數(shù)據(jù)和Using聲明,并關(guān)閉語義檢查,提供一種更加靈活快速的編譯方式。
AssemblyCSharpBuilder builder=new AssemblyCSharpBuilder();
var myAssembly=builder.UseRandomDomain()
.UseSimpleMode()
.Add("public class A{ }")
.GetAssembly();
Natasha也提供了完整覆蓋和部分覆蓋引用和using代碼的能力。例如,合并共享域的引用和using代碼可以使用以下方法:
builder.WithCombineReferences(item=> item.UseAllReferences());
builder.WithCombineUsingCode(UsingLoadBehavior.WithAll);
如果希望合并當(dāng)前域的引用和using代碼或者使用自定義的引用,可以使用如下方法:
builder.WithCurrentReferences();
builder.WithCombineUsingCode(UsingLoadBehavior.WithCurrent);
// 使用自定義的元數(shù)據(jù)引用
builder.WithSpecifiedReferences(someMetadataReferences);
對于編寫和加載腳本,Natasha采用靈活的配置API來覆蓋using代碼,并添加編譯選項(xiàng)。這允許開發(fā)者指定腳本中要使用的C#語言版本,以及如何處理using指令。
// 配置語言版本
builder.ConfigSyntaxOptions(opt=> opt.WithLanguageVersion(LanguageVersion.CSharp6));
// 添加腳本并覆蓋Using Code
builder.WithCombineUsingCode(UsingLoadBehavior.WithAll).Add(myCode);
// 自定義覆蓋Using Code
builder.Add("script", UsingLoadBehavior.WithCurrent);
Natasha提供了一系列的With、Set和Config系列API來精細(xì)控制編譯過程。你可以配置編譯選項(xiàng)、診斷信息級別,甚至啟用或關(guān)閉某些特殊的編譯行為。例如,啟用語義檢查或添加語義處理插件:
// 啟用語義檢查
builder.WithSemanticCheck();
//增加語義處理插件
builder.AddSemanticAnalysistor();
動(dòng)態(tài)調(diào)試
使用Natasha進(jìn)行動(dòng)態(tài)源代碼調(diào)試是輕而易舉的。開啟調(diào)試模式可以幫助你更深入地了解代碼執(zhí)行情況,Natasha提供了多種選項(xiàng)來寫入調(diào)試信息:
builder.WithDebugCompile(item=> item.WriteToFile()); // 調(diào)試信息寫入文件
builder.WithDebugCompile(item=> item.WriteToAssembly()); // 調(diào)試信息整合到程序集
builder.WithReleaseCompile(); // 設(shè)置為Release模式
生成程序集
在程序集被編譯前,你可以使用Natasha提供的API來進(jìn)行各種配置,比如設(shè)置程序集名稱或輸出選項(xiàng):
builder.SetAssemblyName("MyAssembly");
builder.WithSemanticCheck(); // 啟用語義檢查
builder.WithFileOutput("path/to/dll", "path/to/pdb", "path/to/xml"); // 文件輸出配置
Natasha還提供了一個(gè)Codecov擴(kuò)展,可幫助你獲取代碼覆蓋率數(shù)據(jù)。首先你需要引入DotNetCore.Natasha.CSharp.Extension.Codecov
擴(kuò)展包,然后像下面這樣使用:
builder.WithCodecov();
Assembly asm=builder.GetAssembly();
List<(string MethodName, bool[] Usage)>? coverageData=asm.GetCodecovCollection();
Show(coverageData);
上面的Show
方法將遍歷并顯示每個(gè)方法的執(zhí)行情況。這是一種很好的方式來監(jiān)測你的代碼如何執(zhí)行,確保質(zhì)量和可靠性。
最后,Natasha提供了類型擴(kuò)展來幫助你更輕松地處理類型信息。例如,獲取運(yùn)行時(shí)或開發(fā)時(shí)的類型名稱,或者檢查類型是否實(shí)現(xiàn)了某個(gè)接口:
typeof(Dictionary<string,List<int>>[]).GetRuntimeName(); // 獲取運(yùn)行時(shí)類型名稱
typeof(Dictionary<string,List<int>>).IsImplementFrom<IDictionary>(); // 檢查是否實(shí)現(xiàn)指定接口
當(dāng)然這個(gè)項(xiàng)目也是開源的,不論是學(xué)習(xí)思路還是代碼設(shè)計(jì)方案 ,查看下面的項(xiàng)目地址都是不錯(cuò)的選擇
https://github.com/dotnetcore/Natasha
后面我會(huì)使用Natasha嘗試通過AI來生成c#代碼并動(dòng)態(tài)執(zhí)行,可以關(guān)注我,并持續(xù)關(guān)注我的下一步行動(dòng)!
Lapis是一個(gè)為Lua語言設(shè)計(jì)的Web應(yīng)用開發(fā)框架,它主要針對OpenResty,這是一個(gè)基于Nginx的高性能Web平臺。Lapis不僅提供了一個(gè)簡潔而強(qiáng)大的API來構(gòu)建Web服務(wù),還支持現(xiàn)代Web開發(fā)中的多種需求,包括路由、模板、數(shù)據(jù)庫集成、安全性等。
Lapis利用OpenResty的強(qiáng)大性能,通過LuaJIT在Nginx內(nèi)部運(yùn)行Lua代碼,實(shí)現(xiàn)了高性能的處理能力。這意味著開發(fā)者可以享受到接近C語言級別的執(zhí)行效率,同時(shí)保持Lua語言的簡潔性和靈活性。
Lapis支持Lua協(xié)程,允許開發(fā)者編寫看起來是同步的代碼,但實(shí)際上是異步執(zhí)行的。這種方式可以顯著提高應(yīng)用程序的并發(fā)處理能力,同時(shí)避免了回調(diào)地獄,使代碼更加清晰易讀。
Lapis提供了一個(gè)靈活的路由系統(tǒng),允許開發(fā)者定義各種URL模式,并將其映射到相應(yīng)的處理函數(shù)。這使得URL的設(shè)計(jì)和處理變得簡單而直觀。
Lapis內(nèi)置了HTML模板系統(tǒng),支持etlua模板語言,允許開發(fā)者以一種聲明式的方式編寫HTML頁面。此外,Lapis的模板系統(tǒng)還提供了HTML構(gòu)建器語法,使得HTML的生成既安全又便捷。
Lapis支持PostgreSQL、MySQL和SQLite等多種數(shù)據(jù)庫,提供了一個(gè)強(qiáng)大的模型層抽象,使得數(shù)據(jù)庫操作變得簡單。開發(fā)者可以通過繼承Model類來創(chuàng)建自己的數(shù)據(jù)庫模型,并輕松地進(jìn)行數(shù)據(jù)的增刪改查操作。
Lapis提供了CSRF保護(hù)和會(huì)話支持,幫助開發(fā)者構(gòu)建更安全的Web應(yīng)用。通過內(nèi)置的安全特性,可以有效地防止跨站請求偽造等常見的Web安全威脅。
local lapis=require "lapis"
local app=lapis.Application()
app:match("/", function(self)
return "Hello world!"
end)
return app
app:match("/profile/:username", function(self)
local username=self.params.username
return "Welcome, " .. username .. "!"
end)
local lapis=require "lapis"
local app=lapis.Application()
class extends lapis.Application
"/":=>
"Hello world!"
["/profile/:username"]:=>
local username=@params.username
"Welcome, " .. username .. "!"
return app
local Model=require("lapis.db.model").Model
class Users extends Model
local app=lapis.Application()
app:get("/users", function(self)
local users=Users:select("*")
return { render=true, users=users }
end)
return app
local lapis=require "lapis"
local app=lapis.Application()
app:match("/", function(self)
return self:render("index")
end)
return app
Lapis是一個(gè)功能強(qiáng)大且高效的Web開發(fā)框架,它結(jié)合了Lua語言的靈活性和OpenResty的性能優(yōu)勢。無論是構(gòu)建簡單的Web服務(wù)還是復(fù)雜的Web應(yīng)用,Lapis都是一個(gè)值得考慮的選擇。隨著社區(qū)的不斷壯大和生態(tài)系統(tǒng)的完善,Lapis有望成為Lua Web開發(fā)領(lǐng)域的重要力量。
FBI-Analyzer是一個(gè)靈活的日志分析系統(tǒng),基于golang和lua,插件風(fēng)格類似ngx-lua。
使用者只需要編寫簡單的lua邏輯就可以實(shí)現(xiàn)golang能實(shí)現(xiàn)的所有需求,點(diǎn)擊跳轉(zhuǎn)實(shí)現(xiàn)原理。
現(xiàn)實(shí)中可作為WAF的輔助系統(tǒng)進(jìn)行安全分析,點(diǎn)擊跳轉(zhuǎn)實(shí)例。
可快速遷移waf中行為分析插件(非實(shí)時(shí)攔截需求,需要緩存計(jì)算數(shù)據(jù)的邏輯)至本系統(tǒng),避免插件在處理請求時(shí)發(fā)起過多對數(shù)據(jù)緩存(redis等)的請求而導(dǎo)致WAF性能下降,幫助waf減負(fù)。
實(shí)現(xiàn)這個(gè)項(xiàng)目的目的其實(shí)也是加深下對lua虛擬機(jī)的認(rèn)識
以及其他語言通過插件的方式調(diào)用lua腳本的工作原理,本項(xiàng)目因?yàn)橹皇菃渭兊膌ua虛擬機(jī),不是luaJIT,所以不能使用ffi也不能引用三方so的方法。
當(dāng)然使用lua插件化的性能最佳的語言肯定是C,但是因?yàn)樘肆?/p>
所以只能以golang來實(shí)現(xiàn),但是就目前觀察看下來,處理性能還是可以的。
跳過介紹,使用說明點(diǎn)擊跳轉(zhuǎn)。
插件編寫靈活
簡單的需求在配置文件中完成其實(shí)挺不錯(cuò)的
但是在一些較為復(fù)雜的需求面前,配置文件寫出來的可能比較抽象
或者說為了簡化配置就要為某個(gè)單獨(dú)的需求專門在主項(xiàng)目里寫一段專門用來處理的邏輯,可以是可以,但沒必要。
在使用openresty一段時(shí)間后,發(fā)現(xiàn)靈活的插件真的會(huì)減輕不少的工作量。接下來基于一個(gè)相對復(fù)雜的小需求來進(jìn)行插件編寫,點(diǎn)擊跳轉(zhuǎn)插件示例。
需求:對5分鐘內(nèi)的訪問狀態(tài)碼40x的ip進(jìn)行針對統(tǒng)計(jì),5分鐘內(nèi)超過100次的打上標(biāo)簽鎖定10分鐘,供WAF進(jìn)行攔截。
這種肯定也可以在waf中寫插件,但是當(dāng)類似需求多了,那么一條請求處理就可能會(huì)產(chǎn)生多次請求,影響waf性能。
這樣的話只讓waf發(fā)起一條請求讀取下分析結(jié)果就可以直接進(jìn)行攔截,將工作量轉(zhuǎn)移給旁路系統(tǒng),不影響線上服務(wù)。
插件秒級生效
在線上環(huán)境運(yùn)行示例風(fēng)控插件,能涉及到的業(yè)務(wù)總QPS高峰大概有十萬。
(雖然是背著領(lǐng)導(dǎo)偷偷跑的,但是因?yàn)橥耆月酚跇I(yè)務(wù),所以問題不大。
插件目前使用主動(dòng)監(jiān)測的方式進(jìn)行更新(說白了,for循環(huán))
但是其實(shí)可以使用inotify通過修改事件來驅(qū)動(dòng)插件更新
我這里沒寫是因?yàn)槲疫€沒寫完服務(wù)端更新的操作,vim編輯保存文件會(huì)刪除舊文件創(chuàng)建新文件導(dǎo)致文件監(jiān)控失敗,有點(diǎn)憨批所以沒搞。
LogFarmer中實(shí)時(shí)傳日志的方式就是使用事件驅(qū)動(dòng),實(shí)現(xiàn)比較簡單。
插件更新時(shí)會(huì)自動(dòng)編譯緩存,供協(xié)程調(diào)用,避免每次都會(huì)要編譯腳本運(yùn)行。
動(dòng)圖中演示注釋和運(yùn)行打印日志方法來檢測插件生效的速度。
靈活自定義的函數(shù)庫
以打印日志為例
類型是自定義的access日志GoStruct
豐富的三方依賴支撐
golang能夠使用的所有方法都可以被lua使用,通過如上的定義方式,添加進(jìn)lua虛擬機(jī)供lua使用。
例如樣例lua策略腳本中,使用的redis模塊和方法實(shí)際是使用的golang內(nèi)的redis三方庫。
類型是redis.pipeliner
-- 寫成lua的table是這樣
fbi={
var={
__metatable={
__index=getVarFunctin
}
},
log=logFunction,
ERROR=level_error,
}
內(nèi)置全局變量
fbi
類型是redis.pipeliner
-- 寫成lua的table是這樣
fbi={
var={
__metatable={
__index=getVarFunctin
}
},
log=logFunction,
ERROR=level_error,
}
內(nèi)置UserData變量
用于在單個(gè)lua協(xié)程中傳遞變量
access
類型是redis.pipeliner
pipeline
類型是redis.pipeliner
redis
-- 類型都是lua中的類型。ok是bool類型,err是nil或者string類型,result是string或number類型,str是string類型
-- redis單條請求方法
local redis=require("redis")
-- 方法名都和redis方法類似
local result, err=redis.hmget(key, field)
local ok, err=redis.hmset(key, field, value)
local result, err=redis.incr(key, field)
local ok, err=redis.expire(key, second)
local ok, err=redis.delete(key)
-- redis批量請求方法
local redis=require("redis")
local pipeline=redis.pipeline
-- 新建一個(gè)pipeline
pipeline.new()
local result, err=pipeline.hmget(key, field)
local ok, err=pipeline.hmset(key, field, value)
local result, err=pipeline.incr(key, field)
local ok, err=pipeline.expire(key, second)
local ok, err=pipeline.delete(key)
local err=pipeline.exec()
pipeline.close()
re
-- 類型都是lua中的類型。ok是bool類型,err是nil或者string類型,str是string類型
-- 項(xiàng)目在定義給lua用的golang正則方法時(shí),緩存了每個(gè)待匹配模式,比如"^ab",提升速度和性能
local re=require("re")
local ok, err=re.match("abcabcd", "^ab")
local str, err=re.find("abcabcd", "^ab")
time
local time=require("time")
local tu=time.unix() -- 時(shí)間戳
local tf=time.format() -- 格式化時(shí)間 2020-05-31 00:15
local zero=time.zero -- 1590829200, 基準(zhǔn)時(shí)間,用于跟當(dāng)前時(shí)間做差取余算時(shí)間段
說明
目前只寫了kafka的數(shù)據(jù)輸入,且日志格式為json,后期看情況加。
如需對接自家日志,需要在rule/struct.go中定義下日志格式,可以網(wǎng)上找json2gostrcut的轉(zhuǎn)換;
再在lua/http.go對照日志struct進(jìn)行對應(yīng)參數(shù)對接即可。
type AccessLog struct {
Host string `json:"host"` // WAF字段,域名
Status int `json:"status"` // WAF字段,狀態(tài)碼
XFF string `json:"XFF"` // WAF字段,X-Forwarded-for
...
}
// 注意下類型就好,lua里面數(shù)字都是number類型。
func GetReqVar(L *lua.LState) int {
access :=L.GetGlobal("access").(*lua.LUserData).Value.(*rule.AccessLog)
_=L.CheckAny(1)
switch L.CheckString(2) {
case "host":
L.Push(lua.LString(access.Host))
case "status":
L.Push(lua.LNumber(access.Status))
case "XFF":
L.Push(lua.LString(access.XFF))
...
default:
L.Push(lua.LNil)
}
初次使用可通過打印一些變量來測試,例如
local var=fbi.var
local log=fbi.log
local ERROR=fbi.ERROR
log(ERROR, "status is ", tostring(var.status), ", req is ", var.host, var,uri, "?", var.query)
-- 可能輸出 [error] status is 200, req is www.test.com/path/a?id=1
項(xiàng)目運(yùn)行流程
按照go.mod里的配置就行
kafka三方庫需要安裝librdkafka,參照
https://github.com/confluentinc/confluent-kafka-go#installing-librdkafka
redis三方庫前幾天剛更新,每個(gè)執(zhí)行函數(shù)的參數(shù)都加了個(gè)ctx,如果不會(huì)改的話,go get 7.3版本即可
https://github.com/go-redis/redis/tree/v7
現(xiàn)階段軟件配置
日志源:Kafka
數(shù)據(jù)緩存:Redis
配置文件樣例
# redis配置
redis: "127.0.0.1:6379"
password: ""
db: 9
# kafka配置
broker: 192.168.1.1:9092
groupid: group-access-test-v1
topic:
- waflog
offset: latest
# 項(xiàng)目日志配置
path: Analyzer.log
使用方式
git clone https://github.com/C4o/FBI-Analyzer
go build main.go
./main
1.如果沒有redis和kafka,沒有關(guān)系,修改main.go的最后幾行即可。通過print或log方法進(jìn)行輸出。
原始代碼
// 初始化redis,連接和健康檢查
red :=db.Redis{
RedisAddr: conf.Cfg.RedAddr,
RedisPass: conf.Cfg.RedPass,
RedisDB: conf.Cfg.DB,
}
// 初始化kafka配置
kaf :=db.Kafka{
Broker: conf.Cfg.Broker,
GroupID: conf.Cfg.GroupID,
Topic: conf.Cfg.Topic,
Offset: conf.Cfg.Offset,
}
// 啟動(dòng)lua進(jìn)程
for i :=0; i < runtime.NumCPU(); i++ {
go lua.LuaThread(i)
go kaf.Consumer(lua.Kchan, i)
}
// 本地模擬消費(fèi)者,不使用kafka
//go lua.TestConsumer()
// redis健康檢查卡住主進(jìn)程,redis異常斷開程序終止
red.Health()
更新代碼
// 初始化redis,連接和健康檢查
//red :=db.Redis{
// RedisAddr: conf.Cfg.RedAddr,
// RedisPass: conf.Cfg.RedPass,
// RedisDB: conf.Cfg.DB,
//}
// 初始化kafka配置
//kaf :=db.Kafka{
//Broker: conf.Cfg.Broker,
//GroupID: conf.Cfg.GroupID,
//Topic: conf.Cfg.Topic,
//Offset: conf.Cfg.Offset,
//}
// 啟動(dòng)lua進(jìn)程
for i :=0; i < runtime.NumCPU(); i++ {
go lua.LuaThread(i)
//go kaf.Consumer(lua.Kchan, i)
}
// 本地模擬消費(fèi)者,不使用kafka
lua.TestConsumer()
// redis健康檢查卡住主進(jìn)程,redis異常斷開程序終止
// red.Health()
2.如果模塊或參數(shù)使用不對,可在日志中查看lua腳本哪一行報(bào)錯(cuò)。
[root@localhost FBI-Analyzer]# cat Analyzer.log | grep "#" | head -n 5
2020/05/27 13:28:21 [error] Consumer error: 10.205.241.146:9092/bootstrap: Connect to ipv4#10.205.241.146:9092 failed: No route to host (after 4ms in state CONNECT) (<nil>)
2020/05/27 13:41:44 [error] coroutines failed : scripts/counter.lua:5: bad argument #3 to incr (value expected).
2020/05/27 13:41:49 [error] coroutines failed : scripts/counter.lua:5: bad argument #3 to incr (value expected).
2020/05/27 13:41:54 [error] coroutines failed : scripts/counter.lua:5: bad argument #3 to incr (value expected).
2020/05/27 13:41:59 [error] coroutines failed : scripts/counter.lua:5: bad argument #3 to incr (value expected).
WAF體系
攔截中心
項(xiàng)目地址:https://github.com/C4o/IUS
實(shí)時(shí)日志傳輸模塊
項(xiàng)目地址:https://github.com/C4o/LogFarmer
web安全體系化視頻教程,在線免費(fèi)觀看!
滲透視頻教程+進(jìn)群+領(lǐng)工具+靶場
掃碼白嫖!
作者:leviath
轉(zhuǎn)載自:https://www.freebuf.com/sectool/238366.html
*請認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。