diff --git a/build.sh b/build.sh
index b67091bb..eeaf1b8d 100755
--- a/build.sh
+++ b/build.sh
@@ -1,5 +1,5 @@
#/bash/sh
-export VERSION=0.26.2
+export VERSION=0.26.3
sudo apt-get install gcc-mingw-w64-i686
env GOOS=windows GOARCH=386 CGO_ENABLED=1 CC=i686-w64-mingw32-gcc go build -ldflags "-s -w -extldflags -static -extldflags -static" -buildmode=c-shared -o npc_sdk.dll cmd/npc/sdk.go
diff --git a/cmd/npc/npc.go b/cmd/npc/npc.go
index 781dc1f0..67c12546 100644
--- a/cmd/npc/npc.go
+++ b/cmd/npc/npc.go
@@ -15,6 +15,7 @@ import (
"os"
"runtime"
"strings"
+ "sync"
"time"
)
@@ -107,7 +108,12 @@ func main() {
}
s, err := service.New(prg, svcConfig)
if err != nil {
- logs.Error(err)
+ logs.Error(err, "service function disabled")
+ run()
+ // run without service
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+ wg.Wait()
return
}
if len(os.Args) >= 2 {
@@ -172,6 +178,15 @@ func (p *npc) run() error {
logs.Warning("npc: panic serving %v: %v\n%s", err, string(buf))
}
}()
+ run()
+ select {
+ case <-p.exit:
+ logs.Warning("stop...")
+ }
+ return nil
+}
+
+func run() {
common.InitPProfFromArg(*pprofAddr)
//p2p or secret command
if *password != "" {
@@ -187,7 +202,7 @@ func (p *npc) run() error {
commonConfig.Client = new(file.Client)
commonConfig.Client.Cnf = new(file.Config)
go client.StartLocalServer(localServer, commonConfig)
- return nil
+ return
}
env := common.GetEnvMap()
if *serverAddr == "" {
@@ -211,9 +226,4 @@ func (p *npc) run() error {
}
go client.StartFromFile(*configPath)
}
- select {
- case <-p.exit:
- logs.Warning("stop...")
- }
- return nil
}
diff --git a/cmd/nps/nps.go b/cmd/nps/nps.go
index 93640ae7..7746db78 100644
--- a/cmd/nps/nps.go
+++ b/cmd/nps/nps.go
@@ -1,26 +1,27 @@
package main
import (
+ "ehang.io/nps/lib/crypt"
+ "ehang.io/nps/lib/file"
"ehang.io/nps/lib/install"
+ "ehang.io/nps/lib/version"
+ "ehang.io/nps/server"
+ "ehang.io/nps/server/connection"
+ "ehang.io/nps/server/tool"
+ "ehang.io/nps/web/routers"
"flag"
"log"
"os"
"path/filepath"
"runtime"
"strings"
+ "sync"
"ehang.io/nps/lib/common"
- "ehang.io/nps/lib/crypt"
"ehang.io/nps/lib/daemon"
- "ehang.io/nps/lib/file"
- "ehang.io/nps/lib/version"
- "ehang.io/nps/server"
- "ehang.io/nps/server/connection"
- "ehang.io/nps/server/tool"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
- "ehang.io/nps/web/routers"
"github.com/kardianos/service"
)
@@ -97,7 +98,12 @@ func main() {
prg.exit = make(chan struct{})
s, err := service.New(prg, svcConfig)
if err != nil {
- logs.Error(err)
+ logs.Error(err, "service function disabled")
+ run()
+ // run without service
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+ wg.Wait()
return
}
if len(os.Args) > 1 && os.Args[1] != "service" {
@@ -166,6 +172,15 @@ func (p *nps) run() error {
logs.Warning("nps: panic serving %v: %v\n%s", err, string(buf))
}
}()
+ run()
+ select {
+ case <-p.exit:
+ logs.Warning("stop...")
+ }
+ return nil
+}
+
+func run() {
routers.Init()
task := &file.Tunnel{
Mode: "webServer",
@@ -181,9 +196,4 @@ func (p *nps) run() error {
tool.InitAllowPort()
tool.StartSystemInfo()
go server.StartNewServer(bridgePort, task, beego.AppConfig.String("bridge_type"))
- select {
- case <-p.exit:
- logs.Warning("stop...")
- }
- return nil
}
diff --git a/docs/_coverpage.md b/docs/_coverpage.md
index 17d1b602..f055839c 100644
--- a/docs/_coverpage.md
+++ b/docs/_coverpage.md
@@ -1,6 +1,6 @@
![logo](logo.svg)
-# NPS 0.26.2
+# NPS 0.26.3
> 一款轻量级、高性能、功能强大的内网穿透代理服务器
diff --git a/docs/example.md b/docs/example.md
index 2da0b87f..141c2196 100644
--- a/docs/example.md
+++ b/docs/example.md
@@ -108,6 +108,7 @@
**使用步骤**
- 在`nps.conf`中设置`p2p_ip`(nps服务器ip)和`p2p_port`(nps服务器udp端口)
+> 注:若 `p2p_port` 设置为6000,请在防火墙开放6000~6002(额外添加2个端口)udp端口
- 在刚才刚才创建的客户端中添加一条p2p代理,并设置唯一密钥p2pssh
- 在使用端机器(本机)执行命令
diff --git a/docs/faq.md b/docs/faq.md
index a8efdcf3..424e6db8 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -14,3 +14,7 @@
```
install 之后,Linux 配置文件在 /etc/nps
```
+- p2p穿透失败 [p2p服务](https://ehang-io.github.io/nps/#/example?id=p2p%e6%9c%8d%e5%8a%a1)
+```
+双方nat类型都是Symmetric Nat一定不成功,建议先查看nat类型。请按照文档操作(标题上有超链接)
+```
diff --git a/docs/run.md b/docs/run.md
index 805e154c..ca1f3238 100644
--- a/docs/run.md
+++ b/docs/run.md
@@ -26,7 +26,9 @@
## 客户端
- 下载客户端安装包并解压,进入到解压目录
- 点击web管理中客户端前的+号,复制启动命令
-- 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用cmd执行
+- 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用**cmd执行**
+
+如果使用`powershell`运行,**请将ip括起来!**
如果需要注册到系统服务可查看[注册到系统服务](/use?id=注册到系统服务)
diff --git a/docs/use.md b/docs/use.md
index 32c26bdb..53b15c39 100644
--- a/docs/use.md
+++ b/docs/use.md
@@ -17,6 +17,8 @@
- 启动:`npc.exe start`
- 停止:`npc.exe stop`
- 如果需要更换命令内容需要先卸载`npc.exe uninstall`,再重新注册
+- 如果需要当客户端退出时自动重启客户端,请按照如图所示配置
+![image](https://github.com/ehang-io/nps/blob/master/docs/windows_client_service_configuration.png?raw=true)
注册到服务后,日志文件windows位于当前目录下,linux和darwin位于/var/log/npc.log
diff --git a/docs/windows_client_service_configuration.png b/docs/windows_client_service_configuration.png
new file mode 100644
index 00000000..0a1d2c0e
Binary files /dev/null and b/docs/windows_client_service_configuration.png differ
diff --git a/go.mod b/go.mod
index c43c98c4..f8b0288a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module ehang.io/nps
go 1.13
require (
- ehang.io/nps-mux v0.0.0-20200116160632-de59baca47b5
+ ehang.io/nps-mux v0.0.0-20200216160218-8928a6177bac
fyne.io/fyne v1.2.2
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/astaxie/beego v1.12.0
@@ -15,15 +15,16 @@ require (
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db
github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect
github.com/kardianos/service v1.0.0
+ github.com/klauspost/cpuid v1.2.3 // indirect
github.com/klauspost/pgzip v1.2.1 // indirect
github.com/panjf2000/ants/v2 v2.3.0
github.com/pkg/errors v0.9.1
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
github.com/shirou/gopsutil v2.19.11+incompatible
github.com/xtaci/kcp-go v5.4.20+incompatible
- golang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect
- golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
- golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c // indirect
+ golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 // indirect
+ golang.org/x/net v0.0.0-20200202094626-16171245cfb2
+ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
)
replace github.com/astaxie/beego => github.com/exfly/beego v1.12.0-export-init
diff --git a/go.sum b/go.sum
index d4dd9519..f9a87ed9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-ehang.io/nps-mux v0.0.0-20200116160632-de59baca47b5 h1:gbYMN0t1mroAtodN9t7rFRqAYtBGQpqjPNaJ/zFGmD8=
-ehang.io/nps-mux v0.0.0-20200116160632-de59baca47b5/go.mod h1:v2gdtoMBRGYe5y9mSBwPw6V4V/2Zz5GyTuCNlsUPHkY=
+ehang.io/nps-mux v0.0.0-20200216160218-8928a6177bac h1:tNbuf7od+Y/8KfpzhxhJRIROS+CKNG0pJXR3kSmujXs=
+ehang.io/nps-mux v0.0.0-20200216160218-8928a6177bac/go.mod h1:v2gdtoMBRGYe5y9mSBwPw6V4V/2Zz5GyTuCNlsUPHkY=
fyne.io/fyne v1.2.2 h1:mf7EseASp3CAC5vLWVPLnsoKxvp/ARdu3Seh0HvAQak=
fyne.io/fyne v1.2.2/go.mod h1:Ab+3DIB/FVteW0y4DXfmZv4N3JdnCBh2lHkINI02BOU=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -62,6 +62,8 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
+github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
@@ -120,8 +122,8 @@ golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9Qel
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
-golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
+golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -132,15 +134,15 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs=
-golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/gui/npc/AndroidManifest.xml b/gui/npc/AndroidManifest.xml
index 6e6f2d70..8b91063d 100755
--- a/gui/npc/AndroidManifest.xml
+++ b/gui/npc/AndroidManifest.xml
@@ -2,7 +2,7 @@
diff --git a/lib/file/file.go b/lib/file/file.go
index 9e28e95e..0bcf61c3 100644
--- a/lib/file/file.go
+++ b/lib/file/file.go
@@ -99,16 +99,28 @@ func (s *JsonDb) GetClient(id int) (c *Client, err error) {
return
}
+var hostLock sync.Mutex
+
func (s *JsonDb) StoreHostToJsonFile() {
+ hostLock.Lock()
storeSyncMapToFile(s.Hosts, s.HostFilePath)
+ hostLock.Unlock()
}
+var taskLock sync.Mutex
+
func (s *JsonDb) StoreTasksToJsonFile() {
+ taskLock.Lock()
storeSyncMapToFile(s.Tasks, s.TaskFilePath)
+ taskLock.Unlock()
}
+var clientLock sync.Mutex
+
func (s *JsonDb) StoreClientsToJsonFile() {
+ clientLock.Lock()
storeSyncMapToFile(s.Clients, s.ClientFilePath)
+ clientLock.Unlock()
}
func (s *JsonDb) GetClientId() int32 {
@@ -134,7 +146,8 @@ func loadSyncMapFromFile(filePath string, f func(value string)) {
}
func storeSyncMapToFile(m sync.Map, filePath string) {
- file, err := os.Create(filePath)
+ file, err := os.Create(filePath + ".tmp")
+ // first create a temporary file to store
if err != nil {
panic(err)
}
@@ -177,5 +190,7 @@ func storeSyncMapToFile(m sync.Map, filePath string) {
}
return true
})
- file.Sync()
+ _ = file.Sync()
+ err = os.Rename(filePath+".tmp", filePath)
+ // replace the file, maybe provides atomic operation
}
diff --git a/lib/version/version.go b/lib/version/version.go
index b7b9f207..3fbde4b4 100644
--- a/lib/version/version.go
+++ b/lib/version/version.go
@@ -1,6 +1,6 @@
package version
-const VERSION = "0.26.2"
+const VERSION = "0.26.3"
// Compulsory minimum version, Minimum downward compatibility to this version
func GetVersion() string {
diff --git a/server/proxy/socks5.go b/server/proxy/socks5.go
index f2ee2d59..3faefe54 100755
--- a/server/proxy/socks5.go
+++ b/server/proxy/socks5.go
@@ -270,6 +270,7 @@ func (s *Sock5ModeServer) handleUDP(c net.Conn) {
b := common.BufPoolUdp.Get().([]byte)
defer common.BufPoolUdp.Put(b)
+ defer target.Close()
for {
_, err := c.Read(b)
if err != nil {
diff --git a/server/proxy/udp.go b/server/proxy/udp.go
index fa3d0be8..e89259a9 100755
--- a/server/proxy/udp.go
+++ b/server/proxy/udp.go
@@ -1,8 +1,11 @@
package proxy
import (
+ "io"
"net"
"strings"
+ "sync"
+ "time"
"ehang.io/nps/bridge"
"ehang.io/nps/lib/common"
@@ -13,6 +16,7 @@ import (
type UdpModeServer struct {
BaseServer
+ addrMap sync.Map
listener *net.UDPConn
}
@@ -33,8 +37,8 @@ func (s *UdpModeServer) Start() error {
if err != nil {
return err
}
- buf := common.BufPoolUdp.Get().([]byte)
for {
+ buf := common.BufPoolUdp.Get().([]byte)
n, addr, err := s.listener.ReadFromUDP(buf)
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
@@ -49,28 +53,43 @@ func (s *UdpModeServer) Start() error {
}
func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) {
- if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
- logs.Warn("client id %d, task id %d,error %s, when udp connection", s.task.Client.Id, s.task.Id, err.Error())
- return
- }
- defer s.task.Client.AddConn()
- link := conn.NewLink(common.CONN_UDP, s.task.Target.TargetStr, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, addr.String(), s.task.Target.LocalProxy)
- if clientConn, err := s.bridge.SendLinkInfo(s.task.Client.Id, link, s.task); err != nil {
- return
+ if v, ok := s.addrMap.Load(addr.String()); ok {
+ clientConn, ok := v.(io.ReadWriteCloser)
+ if ok {
+ clientConn.Write(data)
+ s.task.Flow.Add(int64(len(data)), 0)
+ }
} else {
- target := conn.GetConn(clientConn, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, nil, true)
- defer target.Close()
- s.task.Flow.Add(int64(len(data)), 0)
- buf := common.BufPoolUdp.Get().([]byte)
- defer common.BufPoolUdp.Put(buf)
- target.Write(data)
- s.task.Flow.Add(int64(len(data)), 0)
- if n, err := target.Read(buf); err != nil {
- logs.Warn(err)
+ if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
+ logs.Warn("client id %d, task id %d,error %s, when udp connection", s.task.Client.Id, s.task.Id, err.Error())
+ return
+ }
+ defer s.task.Client.AddConn()
+ link := conn.NewLink(common.CONN_UDP, s.task.Target.TargetStr, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, addr.String(), s.task.Target.LocalProxy)
+ if clientConn, err := s.bridge.SendLinkInfo(s.task.Client.Id, link, s.task); err != nil {
return
} else {
- s.listener.WriteTo(buf[:n], addr)
- s.task.Flow.Add(0, int64(n))
+ target := conn.GetConn(clientConn, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, nil, true)
+ s.addrMap.Store(addr.String(), target)
+ defer target.Close()
+
+ target.Write(data)
+
+ buf := common.BufPoolUdp.Get().([]byte)
+ defer common.BufPoolUdp.Put(buf)
+
+ s.task.Flow.Add(int64(len(data)), 0)
+ for {
+ clientConn.SetReadDeadline(time.Now().Add(time.Minute * 10))
+ if n, err := target.Read(buf); err != nil {
+ s.addrMap.Delete(addr.String())
+ logs.Warn(err)
+ return
+ } else {
+ s.listener.WriteTo(buf[:n], addr)
+ s.task.Flow.Add(0, int64(n))
+ }
+ }
}
}
}