From bca48d2ea77a7fb5bbcd3162396f2f10ee380e72 Mon Sep 17 00:00:00 2001 From: zyxkad Date: Tue, 24 Oct 2023 15:31:39 -0600 Subject: [PATCH] update liter-proxy --- README_zh.MD | 10 + cmds/liter-proxy/globals.go | 3 + cmds/liter-proxy/handler.go | 373 +++++++++++++++++++++++++++++++++++ cmds/liter-proxy/main.go | 156 --------------- cmds/liter-server/handler.go | 2 +- script/README_zh.MD | 41 ++-- script/types/lib/index.d.ts | 11 +- 7 files changed, 400 insertions(+), 196 deletions(-) create mode 100644 cmds/liter-proxy/handler.go diff --git a/README_zh.MD b/README_zh.MD index 648842d..e82e328 100644 --- a/README_zh.MD +++ b/README_zh.MD @@ -1,4 +1,7 @@ +[![Discord](https://img.shields.io/discord/1158169192114163722?style=for-the-badge&logo=discord&label=SUPPORT)](https://discord.gg/fK5DKwRhPj) +[![Build Status](https://img.shields.io/github/actions/workflow/status/kmcsr/go-liter/build.yml?style=for-the-badge&logo=github&label=Build%20Status)](https://github.com/kmcsr/go-liter/actions) + - [English](./README.MD) - **中文** @@ -6,8 +9,15 @@ 一个可扩展的 Minecraft 代理 +如果您对开发插件感兴趣, 请见 [script/README_zh.MD](./script/README_zh.MD) + ## 功能 - 支持正向代理和反向代理 - 可用 **JavaScript** 扩展 - 使用 [Golang](https://go.dev/) 编写, 快速并且易于理解 + +## 应用程序 + +- [liter-proxy](./cmds/liter-proxy/README.MD): 一个支持 socks5 的 Minecraft 代理 +- [liter-server](./cmds/liter-server/README.MD): 内置一个简易管理面板的 Minecraft 反向代理 diff --git a/cmds/liter-proxy/globals.go b/cmds/liter-proxy/globals.go index f9e6806..1a37496 100644 --- a/cmds/liter-proxy/globals.go +++ b/cmds/liter-proxy/globals.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/kmcsr/go-logger" logrusl "github.com/kmcsr/go-logger/logrus" + "github.com/kmcsr/go-liter" ) var loger = initLogger() @@ -99,3 +100,5 @@ func readConfig()(cfg Config){ } return } + +var AuthClient = liter.DefaultAuthClient diff --git a/cmds/liter-proxy/handler.go b/cmds/liter-proxy/handler.go new file mode 100644 index 0000000..dbcaa50 --- /dev/null +++ b/cmds/liter-proxy/handler.go @@ -0,0 +1,373 @@ + +package main + +import ( + "fmt" + "io" + "net" + "strings" + "time" + + "golang.org/x/net/proxy" + "github.com/kmcsr/go-logger" + "github.com/kmcsr/go-liter" + "github.com/kmcsr/go-liter/script" +) + +func (s *ProxyServer)handle(c *liter.Conn){ + preventCliSideClose := false + wc := manager.WrapConn(c) + defer func(){ + if !preventCliSideClose { + wc.Close() + } + }() + rc := c.RawConn() + + wc.OnClose = func(){ + wc.Emit(&script.Event{ Name: "close", Data: Map{ "conn": wc.Exports() } }) + } + + + ploger := logger.NewPrefixLogger(loger, "client [%v]:", c.RemoteAddr()) + ploger.Debugf("Connected!") + var err error + var hp *liter.HandshakePkt + rc.SetReadDeadline(time.Now().Add(time.Second * 5)) + if hp, err = c.RecvHandshakePkt(); err != nil { + ploger.Debugf("Read handshake packet error: %v", err) + return + } + rc.SetReadDeadline(time.Time{}) + ploger.Debugf("Handshake packet: %v", hp) + isLogin := hp.NextState == liter.NextLoginState + + item, ok := cfg.ProxyMap[hp.Addr] + if !ok { + ploger.Infof("Trying to connect with unexpected address %q", hp.Addr) + return + } + + var outHp *liter.HandshakePkt + *outHp = *hp + if item.ForwardAddr != "" { + outHp.Addr = item.ForwardAddr + } + if item.ForwardPort != 0 { + outHp.Port = item.ForwardPort + } + + noforward := <-manager.Emit(script.NewEvent("handshake", Map{ + "client": wc.Exports(), + "handshake": hp, + "target": item.Target, + })) + if wc.Closed() { + return + } + if noforward { + for !wc.Closed() { + var r *script.WrappedPacketReader + if r, err = wc.Recv(); err != nil { + if !wc.Closed() { + wc.Emit(&script.Event{ + Name: "error", + Data: Map{ + "conn": wc.Exports(), + "error": err.Error(), + }, + }) + } + return + } + wc.Emit(script.NewEvent("packet", Map{ + "conn": wc, + "packet": r, + })) + } + return + } + + var lp liter.LoginStartPkt + var player Map // only exists when login + + if isLogin { + if err = c.RecvPkt(0x00, &lp); err != nil { + ploger.Debugf("Read login start packet error: %v", err) + return + } + }else{ + // unknown type connection + return + } + + var addr *net.TCPAddr + if addr, err = liter.ResloveAddrWithContext(s.ctx, item.Target); err != nil { + ploger.Errorf("Cannot resolve addr of %q: %v", item.Target, err) + return + } + var cr net.Conn + if s.Dialer == nil { + cr, err = proxy.Dial(s.ctx, "tcp", addr.String()) + }else{ + cr, err = s.Dialer.DialContext(s.ctx, "tcp", addr.String()) + } + if err != nil { + ploger.Errorf("Cannot dial to %q: %v", item.Target, err) + return + } + conn := liter.WrapConn(cr) + wconn := manager.WrapConn(conn) + preventSvrSideClose := false + defer func(){ + if !preventSvrSideClose { + wconn.Close() + } + }() + wconn.OnClose = func(){ + wconn.Emit(&script.Event{ Name: "close", Data: Map{ "conn": wconn.Exports() } }) + } + + if err = conn.SendHandshakePkt(hp); err != nil { + ploger.Errorf("Connection handshake error: %v", err) + return + } + ploger.Debugf("Handshake sent successed") + + if isLogin { + if err = conn.SendPkt(0x00, lp); err != nil { + ploger.Errorf("Connection login error: %v", err) + return + } + ploger.Debugf("Login start packet sent successed") + } + + if !<-manager.Emit(script.NewEvent("serve", Map{ + "client": wc.Exports(), + "server": wconn.Exports(), + "player": player, + "handshake": hp, + })) { + if proxyRawConn(ploger, cr, rc) { + // if connection reset by peer + if isLogin { + loginDisconnect(c, "Connection reset by peer") + } + } + return + } + + var sp *liter.LoginSuccessPkt + if sp, err = proxyLoginPackets(s, conn, c); err != nil { + ploger.Debugf("Cannot login: %v", err) + return + } + _ = sp + + errCh1 := parseAndForward(wconn, wc) + errCh2 := parseAndForward(wc, wconn) + select { + case err := <-errCh1: + ploger.Errorf("Error at client connection: %v", err) + if <-wconn.Emit(script.NewEvent("before_close", Map{ + "conn": wconn.Exports(), + "error": err.Error(), + })) { + ploger.Infof("Server connection default close action prevented") + preventSvrSideClose = true + } + case err := <-errCh2: + ploger.Errorf("Error at server connection: %v", err) + if <-wc.Emit(script.NewEvent("before_close", Map{ + "conn": wc.Exports(), + "error": err.Error(), + })) { + ploger.Infof("Client connection default close action prevented") + preventCliSideClose = true + } + } +} + + +func handleServerStatus(loger logger.Logger, c *liter.Conn, status string, motd string){ + var srp liter.StatusRequestPkt + var err error + if err = c.RecvPkt(0x00, &srp); err != nil { + loger.Debugf("Read status request packet error: %v", err) + return + } + if err = c.SendPkt(0x00, liter.StatusResponsePkt{ + Payload: liter.StatusResponsePayload{ + Version: liter.ProtocolVersion{ + Name: status, + Protocol: c.Protocol(), + }, + Players: liter.PlayerStatus{ + Max: 2, + Online: 1, + Sample: []liter.PlayerInfo{ + { Name: status }, // to allow hover for the status + }, + }, + Description: liter.NewChatFromString(motd), + }, + }); err != nil { + loger.Debugf("Send packet error: %v", err) + return + } + var prp liter.PingRequestPkt + if err = c.RecvPkt(0x01, &prp); err != nil { + loger.Debugf("Read ping request packet error: %v", err) + return + } + if err = c.SendPkt(0x01, (liter.PingResponsePkt)(prp)); err != nil { + loger.Debugf("Send ping response packet error: %v", err) + return + } +} + +func proxyRawConn(ploger logger.Logger, cr, rc net.Conn)(bool){ + buf := make([]byte, 32 * 1024) + cr.SetReadDeadline(time.Now().Add(time.Millisecond * 10)) + // try read to ensure the connection is ok + if n, err := cr.Read(buf); err != nil { + if strings.Contains(err.Error(), "reset by peer") { + ploger.Errorf("Connection reset by peer") + return true + } + }else if n != 0 { + rc.Write(buf[:n]) + } + cr.SetReadDeadline(time.Time{}) // clear read deadline + + go func(){ + defer cr.Close() + defer rc.Close() + buf := make([]byte, 32 * 1024) + io.CopyBuffer(rc, cr, buf) + }() + io.CopyBuffer(cr, rc, buf) + return false +} + +func loginDisconnectByErr(c *liter.Conn, e error)(err error){ + return c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString(e.Error()), + }) +} + +func loginDisconnect(c *liter.Conn, reason string)(err error){ + return c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString(reason), + }) +} + +type DisconnectError struct { + Reason *liter.Chat +} + +func (e *DisconnectError)Error()(string){ + return e.Reason.Plain() +} + +func proxyLoginPackets(s *ProxyServer, svr, cli *liter.Conn)(res *liter.LoginSuccessPkt, err error){ + res = new(liter.LoginSuccessPkt) + var r *liter.PacketReader + if r, err = svr.Recv(); err != nil { + loginDisconnectByErr(cli, err) + return + } + switch r.Id() { + case 0x00: // Disconnect + var pkt liter.DisconnectPkt + if err = pkt.DecodeFrom(r); err != nil { + loginDisconnectByErr(cli, err) + return + } + if err = cli.SendPkt(0x00, &pkt); err != nil { + return + } + return nil, &DisconnectError{ Reason: pkt.Reason } + case 0x03: // Set Compression + var pkt liter.LoginSetCompressionPkt + if err = pkt.DecodeFrom(r); err != nil { + loginDisconnectByErr(cli, err) + return + } + if err = cli.SendPkt(0x03, &pkt); err != nil { + return + } + if pkt.Threshold >= 0 { + cli.SetThreshold((int)(pkt.Threshold)) + svr.SetThreshold((int)(pkt.Threshold)) + } + if r, err = svr.Recv(); err != nil { + loginDisconnectByErr(cli, err) + return + } + if r.Id() != 0x02 { + err = &liter.PktIdAssertError{ Require: 0x02, Got: (int32)(r.Id()) } + return + } + fallthrough + case 0x02: // Login Success + if err = res.DecodeFrom(r); err != nil { + loginDisconnectByErr(cli, err) + return + } + if err = cli.SendPkt(0x02, res); err != nil { + return + } + return + // case 0x01: // Encryption Request + default: + err = fmt.Errorf("Unexpected packet id %d", r.Id()) + loginDisconnectByErr(cli, err) + return + } + return +} + +func parseAndForward(dst, src *script.WrappedConn)(<-chan error){ + errCh := make(chan error, 1) + go func(){ + var err error + defer func(){ + errCh <- err + }() + var pkt liter.PacketBuilder + for !src.Closed() { + var r *script.WrappedPacketReader + if r, err = src.Recv(); err != nil { + if !src.Closed() { + src.Emit(&script.Event{ + Name: "error", + Data: Map{ + "conn": src.Exports(), + "error": err.Error(), + }, + }) + } + return + } + if !<-src.Emit(script.NewEvent("packet", Map{ + "conn": src.Exports(), + "packet": r, + })) { + if err = dst.Send(pkt.Reset(r.Protocol(), r.Id()).ByteArray(r.Bytes())); err != nil { + if !dst.Closed() { + dst.Emit(&script.Event{ + Name: "error", + Data: Map{ + "conn": dst.Exports(), + "error": err.Error(), + }, + }) + } + return + } + } + } + }() + return errCh +} diff --git a/cmds/liter-proxy/main.go b/cmds/liter-proxy/main.go index 236eada..f7a8c45 100644 --- a/cmds/liter-proxy/main.go +++ b/cmds/liter-proxy/main.go @@ -4,7 +4,6 @@ package main import ( "context" "errors" - "io" "net" "net/url" "os" @@ -161,158 +160,3 @@ func (s *ProxyServer)Shutdown(ctx context.Context)(err error){ return ctx.Err() } } - -func (s *ProxyServer)handle(c *liter.Conn){ - defer c.Close() - s.conns.Add(c) - defer s.conns.Del(c) - - wc := manager.WrapConn(c) - - ploger := logger.NewPrefixLogger(loger, "client [%v]:", c.RemoteAddr()) - ploger.Infof("Connected") - - var err error - var hp *liter.HandshakePkt - if hp, err = c.RecvHandshakePkt(); err != nil { - ploger.Errorf("Read handshake packet error: %v", err) - return - } - ploger.Tracef("Handshake packet: %v", hp) - - item, ok := cfg.ProxyMap[hp.Addr] - if !ok { - return - } - - var outHp *liter.HandshakePkt - *outHp = *hp - if item.ForwardAddr != "" { - outHp.Addr = item.ForwardAddr - } - if item.ForwardPort != 0 { - outHp.Port = item.ForwardPort - } - - noforward := <-manager.Emit(script.NewEvent("handshake", Map{ - "client": wc, - "handshake": hp, - "target": &item, // to allow changes to the target - })) - if wc.Closed() { - return - } - if noforward { - for !wc.Closed() { - var r *script.WrappedPacketReader - if r, err = wc.Recv(); err != nil { - if !wc.Closed() { - wc.Emit(&script.Event{ - Name: "error", - Data: Map{ - "conn": wc, - "error": err.Error(), - }, - }) - } - return - } - <-wc.Emit(script.NewEvent("packet", Map{ - "conn": wc, - "packet": r, - })) - } - return - } - - var addr *net.TCPAddr - if addr, err = liter.ResloveAddrWithContext(s.ctx, item.Target); err != nil { - ploger.Errorf("Cannot resolve addr of %q: %v", item.Target, err) - return - } - var rawconn net.Conn - if s.Dialer == nil { - rawconn, err = proxy.Dial(s.ctx, "tcp", addr.String()) - }else{ - rawconn, err = s.Dialer.DialContext(s.ctx, "tcp", addr.String()) - } - if err != nil { - ploger.Errorf("Cannot dial to %q: %v", item.Target, err) - return - } - - conn := liter.WrapConn(rawconn) - if err = conn.SendHandshakePkt(outHp); err != nil { - ploger.Errorf("New connection handshake error: %v", err) - return - } - - wconn := manager.WrapConn(conn) - - if !<-manager.Emit(script.NewEvent("serve", Map{ - "client": wc, - "server": wconn, - "handshake": hp, - })) { - rc, cr := c.RawConn(), conn.RawConn() - go io.Copy(rc, cr) - io.Copy(cr, rc) - return - } - - done := make(chan struct{}, 2) - go func(){ - defer func(){ - done <- struct{}{} - }() - parseAndForward(wconn, wc) - }() - go func(){ - defer func(){ - done <- struct{}{} - }() - parseAndForward(wc, wconn) - }() - <-done - <-done -} - -func parseAndForward(dst, src *script.WrappedConn)(err error){ - var pkt liter.PacketBuilder - for !src.Closed() { - var r *script.WrappedPacketReader - if r, err = src.Recv(); err != nil { - if !src.Closed() { - src.Emit(&script.Event{ - Name: "error", - Data: Map{ - "src": src, - "dst": dst, - "error": err.Error(), - }, - }) - } - return - } - if !<-src.Emit(script.NewEvent("packet", Map{ - "src": src, - "dest": dst, - "packet": r, - })) { - if err = dst.Send(pkt.Reset(r.Protocol(), r.Id()).ByteArray(r.Bytes())); err != nil { - if !dst.Closed() { - dst.Emit(&script.Event{ - Name: "error", - Data: Map{ - "src": src, - "dst": dst, - "error": err.Error(), - }, - }) - } - } - return - } - } - return -} diff --git a/cmds/liter-server/handler.go b/cmds/liter-server/handler.go index 9a024ed..3641015 100644 --- a/cmds/liter-server/handler.go +++ b/cmds/liter-server/handler.go @@ -65,7 +65,7 @@ func (s *Server)handle(c *liter.Conn, cfg *Config){ noforward := <-s.scripts.Emit(script.NewEvent("handshake", Map{ "client": wc.Exports(), "handshake": hp, - "target": *svr, // do not allow changes + "target": svr.Target, })) if wc.Closed() { return diff --git a/script/README_zh.MD b/script/README_zh.MD index 18e774e..cc905ca 100644 --- a/script/README_zh.MD +++ b/script/README_zh.MD @@ -4,46 +4,29 @@ # Script -该包实现了脚本管理器等功能 - -每个插件都应放在工作目录下的`plugins`文件夹内, 文件名以`.js`结尾并符合如下格式: -```regexp -^([a-z_][0-9a-z_]{0,31})(?:@(\d+(?:\.\d+)*))?(?:-.+)?\..+$ -``` -简单来说, 文件名分为3个部分: -1. 插件ID: 该部分必须使用小写字母或下划线开头, 仅能包含数字、小写字母或下划线. 长度在1~32之间 -2. 插件版本 (可选): 该部分必须以 `@` 开头, 仅包含数字和小数点 -3. 附加信息: 该部分以小数点(`.`)或横杠(`-`)开头, 直到文件名末尾 +该包实现了脚本管理器等功能. +插件基于 [goja](https://github.com/dop251/goja) 运行, 然而 goja 并不支持全部的 ES6 特性, 请开发者注意. 每个脚本拥有独立的魔法变量 `$`, 该变量提供脚本的基本信息, 如 `$.ID`、`$.VERSION`. `$.storage` 实现了 [Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Storage) 目前仅在运行时有效, 暂不推荐使用. 脚本可以使用 `$.on`、`$.emit` 等方法收发事件 -### 日志 - -所有脚本日志都应该通过 `console` 对象输出. -`trace`、`debug`、`info`/`log`、`warn`、`error` 6个方法代表了日志的5个级别, 级别过低的日志在没有明确配置的情况下不会被记录. +# 初始化 -日志输出时会自动加上脚本ID作为前缀. +```sh +npm init glp@latest "" +``` -### 事件系统 +注: 同时会自动配置 Typescript -`EventEmitter` 对象实现了 [NodeJS EventEmitter](https://nodejs.dev/en/learn/the-nodejs-event-emitter/) 的大部分功能. +# 日志 -`EventEmitter` 的监听器有且仅传入一个参数 `event`, `Event` 对象的定义如下: -```ts -interface Event { - name: string - data: Object - readonly cancelable: boolean - readonly canceled: boolean - cancel(): void -} -``` +所有脚本日志都应该通过 `console` 对象输出. +`trace`、`debug`、`info`/`log`、`warn`、`error` 6个方法代表了日志的5个级别, 级别过低的日志在没有明确配置的情况下不会被记录. -发送事件时, `emit(, [], [])` 会自动打包成 `Event` 对象 +日志输出时会自动加上脚本ID作为前缀. -大部分对象都实现了 `EventEmitter`, 常见事件有: +# 常见事件 | 所在对象 | 事件名 | 描述 | 撤销后果 | |---------|-------------|--------------|-----------------| diff --git a/script/types/lib/index.d.ts b/script/types/lib/index.d.ts index 0f9ccb7..194fc20 100644 --- a/script/types/lib/index.d.ts +++ b/script/types/lib/index.d.ts @@ -113,19 +113,10 @@ declare global { nextState: number } - interface ServerIns { - id: string - target: string - serverNames: string[] - handlePing: boolean - motd: string - motdFailed: string - } - type HandshakeEvent = Event<{ client: Conn handshake: Readonly - target: Readonly + target: string }> type ServeEvent = Event<{