Commit f20ad492 authored by rui.zheng's avatar rui.zheng

fix socks5 tcp bind

parent 28089dfb
...@@ -58,6 +58,7 @@ func listenAndServe(arg Args) error { ...@@ -58,6 +58,7 @@ func listenAndServe(arg Args) error {
return listenAndServeUdpForward(arg) return listenAndServeUdpForward(arg)
case "rtcp": // Remote TCP port forwarding case "rtcp": // Remote TCP port forwarding
return serveRTcpForward(arg) return serveRTcpForward(arg)
case "rudp": // Remote UDP port forwarding
default: default:
ln, err = net.Listen("tcp", arg.Addr) ln, err = net.Listen("tcp", arg.Addr)
} }
...@@ -78,49 +79,6 @@ func listenAndServe(arg Args) error { ...@@ -78,49 +79,6 @@ func listenAndServe(arg Args) error {
} }
} }
func serveRTcpForward(arg Args) error {
if arg.Forward == "" {
ln, err := net.Listen("tcp", arg.Addr)
if err != nil {
return err
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
glog.V(LWARNING).Infoln(err)
continue
}
tc := conn.(*net.TCPConn)
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(time.Second * 60)
go handleRTcpForwardConn(conn, arg)
}
} else {
retry := 0
for {
conn, err := Connect(arg.Forward)
if err != nil {
glog.V(LWARNING).Infof("[rtcp] %s -> %s : %s", arg.Addr, arg.Forward, err)
time.Sleep((1 << uint(retry)) * time.Second)
if retry < 5 {
retry++
}
continue
}
retry = 0
if err := connectRTcpForward(conn, arg); err != nil {
conn.Close()
time.Sleep(10 * time.Second)
}
}
}
}
func listenAndServeTcpForward(arg Args) error { func listenAndServeTcpForward(arg Args) error {
ln, err := net.Listen("tcp", arg.Addr) ln, err := net.Listen("tcp", arg.Addr)
if err != nil { if err != nil {
...@@ -165,6 +123,31 @@ func listenAndServeUdpForward(arg Args) error { ...@@ -165,6 +123,31 @@ func listenAndServeUdpForward(arg Args) error {
} }
} }
func serveRTcpForward(arg Args) error {
if len(forwardArgs) == 0 {
return errors.New("rtcp: at least one -F must be assigned")
}
retry := 0
for {
conn, _, err := forwardChain(forwardArgs...)
if err != nil {
glog.V(LWARNING).Infof("[rtcp] %s - %s : %s", arg.Addr, arg.Remote, err)
time.Sleep((1 << uint(retry)) * time.Second)
if retry < 5 {
retry++
}
continue
}
retry = 0
if err := connectRTcpForward(conn, arg); err != nil {
conn.Close()
time.Sleep(10 * time.Second)
}
}
}
func handleConn(conn net.Conn, arg Args) { func handleConn(conn net.Conn, arg Args) {
atomic.AddInt32(&connCounter, 1) atomic.AddInt32(&connCounter, 1)
glog.V(LDEBUG).Infof("%s connected, connections: %d", glog.V(LDEBUG).Infof("%s connected, connections: %d",
...@@ -207,7 +190,7 @@ func handleConn(conn net.Conn, arg Args) { ...@@ -207,7 +190,7 @@ func handleConn(conn net.Conn, arg Args) {
conn = gosocks5.ServerConn(conn, selector) conn = gosocks5.ServerConn(conn, selector)
req, err := gosocks5.ReadRequest(conn) req, err := gosocks5.ReadRequest(conn)
if err != nil { if err != nil {
glog.V(LWARNING).Infoln("[socks5] request:", err) glog.V(LWARNING).Infoln("[socks5]", err)
return return
} }
handleSocks5Request(req, conn) handleSocks5Request(req, conn)
...@@ -298,9 +281,6 @@ func Connect(addr string) (conn net.Conn, err error) { ...@@ -298,9 +281,6 @@ func Connect(addr string) (conn net.Conn, err error) {
var end Args var end Args
conn, end, err = forwardChain(forwardArgs...) conn, end, err = forwardChain(forwardArgs...)
if err != nil { if err != nil {
if conn != nil {
conn.Close()
}
return nil, err return nil, err
} }
if err := establish(conn, addr, end); err != nil { if err := establish(conn, addr, end); err != nil {
...@@ -312,6 +292,13 @@ func Connect(addr string) (conn net.Conn, err error) { ...@@ -312,6 +292,13 @@ func Connect(addr string) (conn net.Conn, err error) {
// establish connection throughout the forward chain // establish connection throughout the forward chain
func forwardChain(chain ...Args) (conn net.Conn, end Args, err error) { func forwardChain(chain ...Args) (conn net.Conn, end Args, err error) {
defer func() {
if err != nil && conn != nil {
conn.Close()
conn = nil
}
}()
end = chain[0] end = chain[0]
if conn, err = net.DialTimeout("tcp", end.Addr, time.Second*90); err != nil { if conn, err = net.DialTimeout("tcp", end.Addr, time.Second*90); err != nil {
return return
......
This diff is collapsed.
...@@ -45,8 +45,7 @@ func main() { ...@@ -45,8 +45,7 @@ func main() {
return return
} }
if pv { if pv {
fmt.Fprintln(os.Stderr, "gost", Version) fmt.Fprintf(os.Stderr, "gost %s (%s)\n", Version, runtime.Version())
fmt.Fprintln(os.Stderr, runtime.Version())
return return
} }
......
...@@ -192,18 +192,67 @@ func handleSocks5Request(req *gosocks5.Request, conn net.Conn) { ...@@ -192,18 +192,67 @@ func handleSocks5Request(req *gosocks5.Request, conn net.Conn) {
glog.V(LINFO).Infof("[socks5-connect] %s <-> %s", conn.RemoteAddr(), req.Addr) glog.V(LINFO).Infof("[socks5-connect] %s <-> %s", conn.RemoteAddr(), req.Addr)
Transport(conn, tconn) Transport(conn, tconn)
glog.V(LINFO).Infof("[socks5-connect] %s >-< %s", conn.RemoteAddr(), req.Addr) glog.V(LINFO).Infof("[socks5-connect] %s >-< %s", conn.RemoteAddr(), req.Addr)
case gosocks5.CmdBind: case gosocks5.CmdBind:
glog.V(LINFO).Infof("[socks5-bind] %s - %s", conn.RemoteAddr(), req.Addr) glog.V(LINFO).Infof("[socks5-bind] %s - %s", conn.RemoteAddr(), req.Addr)
if len(forwardArgs) > 0 { reply, fconn, err := socks5Bind(req, conn)
forwardBind(req, conn) if reply != nil {
} else { if err := reply.Write(conn); err != nil {
tc := conn.(*net.TCPConn) glog.V(LWARNING).Infof("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
tc.SetKeepAlive(true) if fconn != nil {
tc.SetKeepAlivePeriod(time.Second * 60) fconn.Close()
}
return
}
glog.V(LDEBUG).Infof("[socks5-bind] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, reply)
}
serveBind(req, conn) if err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s - %s : %s", conn.RemoteAddr(), req.Addr, err)
return
} }
defer fconn.Close()
glog.V(LINFO).Infof("[socks5-bind] %s <-> %s", conn.RemoteAddr(), fconn.RemoteAddr())
Transport(conn, fconn)
glog.V(LINFO).Infof("[socks5-bind] %s >-< %s", conn.RemoteAddr(), fconn.RemoteAddr())
/*
case gosocks5.CmdUdp:
case CmdUdpTun:
glog.V(LINFO).Infof("[socks5-udp] %s - %s ASSOCIATE", conn.RemoteAddr(), req.Addr)
if len(forwardArgs) > 0 { // direct forward
fconn, _, err := forwardChain(forwardArgs...)
if err != nil {
glog.V(LWARNING).Infof("[socks5-udp] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err)
rep := gosocks5.NewReply(gosocks5.Failure, nil)
if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5-udp] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
} else {
glog.V(LDEBUG).Infof("[socks5-udp] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep)
}
return
}
defer fconn.Close()
if err := req.Write(fconn); err != nil {
glog.V(LWARNING).Infof("[socks5-udp] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err)
rep := gosocks5.NewReply(gosocks5.Failure, nil)
if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5-udp] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
} else {
glog.V(LDEBUG).Infof("[socks5-udp] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep)
}
return
}
glog.V(LINFO).Infof("[socks5-udp] %s <-> %s", conn.RemoteAddr(), req.Addr)
Transport(conn, fconn)
glog.V(LINFO).Infof("[socks5-udp] %s >-< %s", conn.RemoteAddr(), req.Addr)
} else {
}
*/
case gosocks5.CmdUdp, CmdUdpTun: case gosocks5.CmdUdp, CmdUdpTun:
// TODO: udp tunnel <-> forward chain // TODO: udp tunnel <-> forward chain
glog.V(LINFO).Infof("[socks5-udp] %s - %s ASSOCIATE", conn.RemoteAddr(), req.Addr) glog.V(LINFO).Infof("[socks5-udp] %s - %s ASSOCIATE", conn.RemoteAddr(), req.Addr)
...@@ -296,35 +345,35 @@ func handleSocks5Request(req *gosocks5.Request, conn net.Conn) { ...@@ -296,35 +345,35 @@ func handleSocks5Request(req *gosocks5.Request, conn net.Conn) {
} }
} }
func serveBind(req *gosocks5.Request, conn net.Conn) error { func socks5Bind(req *gosocks5.Request, conn net.Conn) (*gosocks5.Reply, net.Conn, error) {
if len(forwardArgs) > 0 {
fconn, _, err := forwardChain(forwardArgs...)
if err != nil {
return gosocks5.NewReply(gosocks5.Failure, nil), nil, err
}
if err := req.Write(fconn); err != nil {
fconn.Close()
return gosocks5.NewReply(gosocks5.Failure, nil), nil, err
}
return nil, fconn, nil
}
bindAddr, _ := net.ResolveTCPAddr("tcp", req.Addr.String()) bindAddr, _ := net.ResolveTCPAddr("tcp", req.Addr.String())
ln, err := net.ListenTCP("tcp", bindAddr) ln, err := net.ListenTCP("tcp", bindAddr)
if err != nil { if err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err) return gosocks5.NewReply(gosocks5.Failure, nil), nil, err
if bindAddr != nil {
ln, err = net.ListenTCP("tcp", nil)
}
if err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err)
rep := gosocks5.NewReply(gosocks5.Failure, nil)
if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
} else {
glog.V(LDEBUG).Infof("[socks5-bind] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep)
}
return err
}
} }
addr := ToSocksAddr(ln.Addr()) addr := ToSocksAddr(ln.Addr())
// Issue: may not reachable when host has two interfaces // Issue: may not reachable when host has two interfaces
addr.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String()) addr.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String())
rep := gosocks5.NewReply(gosocks5.Succeeded, addr) rep := gosocks5.NewReply(gosocks5.Succeeded, addr)
if err := rep.Write(conn); err != nil { if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err) glog.V(LWARNING).Infof("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
ln.Close() ln.Close()
return err return nil, nil, err
} }
glog.V(LDEBUG).Infof("[socks5-bind] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep) glog.V(LDEBUG).Infof("[socks5-bind] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep)
glog.V(LINFO).Infof("[socks5-bind] %s - %s BIND ON %s OK", conn.RemoteAddr(), req.Addr, addr) glog.V(LINFO).Infof("[socks5-bind] %s - %s BIND ON %s OK", conn.RemoteAddr(), req.Addr, addr)
...@@ -342,7 +391,9 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error { ...@@ -342,7 +391,9 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error {
peerChan := make(chan error, 1) peerChan := make(chan error, 1)
go func() { go func() {
defer close(peerChan) defer close(peerChan)
_, err := ioutil.ReadAll(conn) b := tcpPool.Get().([]byte)
defer tcpPool.Put(b)
_, err := conn.Read(b)
if err != nil { if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
return return
...@@ -358,14 +409,8 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error { ...@@ -358,14 +409,8 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error {
case c := <-lnChan: case c := <-lnChan:
ln.Close() // only accept one peer ln.Close() // only accept one peer
if c == nil { if c == nil {
if err := gosocks5.NewReply(gosocks5.Failure, nil).Write(conn); err != nil { return gosocks5.NewReply(gosocks5.Failure, nil), nil, errors.New("[socks5-bind] accept error")
glog.V(LWARNING).Infoln("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), addr, err)
}
glog.V(LWARNING).Infof("[socks5-bind] %s >-< %s", conn.RemoteAddr(), addr)
return errors.New("accept error")
} }
// glog.V(LINFO).Infof("[socks5-bind] %s <- %s PEER %s ACCEPTED", conn.RemoteAddr(), req.Addr, c.RemoteAddr())
// gosocks5.NewReply(gosocks5.Succeeded, ToSocksAddr(c.RemoteAddr())).Write(conn)
pconn = c pconn = c
lnChan = nil lnChan = nil
ln = nil ln = nil
...@@ -378,97 +423,22 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error { ...@@ -378,97 +423,22 @@ func serveBind(req *gosocks5.Request, conn net.Conn) error {
if pconn != nil { if pconn != nil {
pconn.Close() pconn.Close()
} }
glog.V(LWARNING).Infof("[socks5-bind] %s >-< %s", conn.RemoteAddr(), addr) if err == nil {
return err err = errors.New("Oops, some mysterious error!")
}
return nil, nil, err
} }
goto out goto out
} }
} }
out: out:
defer pconn.Close()
conn.SetReadDeadline(time.Time{}) conn.SetReadDeadline(time.Time{})
paddr := ToSocksAddr(pconn.RemoteAddr()) glog.V(LINFO).Infof("[socks5-bind] %s <- %s PEER %s ACCEPTED", conn.RemoteAddr(), addr, pconn.RemoteAddr())
glog.V(LINFO).Infof("[socks5-bind] %s <- %s PEER %s ACCEPTED", conn.RemoteAddr(), addr, paddr) rep = gosocks5.NewReply(gosocks5.Succeeded, ToSocksAddr(pconn.RemoteAddr()))
return rep, pconn, nil
rep = gosocks5.NewReply(gosocks5.Succeeded, paddr)
if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5 bind] %s <- %s : %s", conn.RemoteAddr(), addr, err)
return err
}
glog.V(LDEBUG).Infof("[socks5 bind] %s <- %s\n%s", conn.RemoteAddr(), addr, rep)
glog.V(LINFO).Infof("[socks5-bind] %s <-> %s", conn.RemoteAddr(), paddr)
defer glog.V(LINFO).Infof("[socks5-bind] %s >-< %s", conn.RemoteAddr(), paddr)
return Transport(conn, pconn)
}
func forwardBind(req *gosocks5.Request, conn net.Conn) error {
fconn, _, err := forwardChain(forwardArgs...)
if err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err)
if fconn != nil {
fconn.Close()
}
rep := gosocks5.NewReply(gosocks5.Failure, nil)
if err := rep.Write(conn); err != nil {
glog.V(LWARNING).Infof("[socks5-bind] %s <- %s : %s", conn.RemoteAddr(), req.Addr, err)
} else {
glog.V(LDEBUG).Infof("[socks5-bind] %s <- %s\n%s", conn.RemoteAddr(), req.Addr, rep)
}
return err
}
defer fconn.Close()
if err := req.Write(fconn); err != nil {
glog.V(LWARNING).Infoln("[socks5-bind] %s -> %s : %s", conn.RemoteAddr(), req.Addr, err)
gosocks5.NewReply(gosocks5.Failure, nil).Write(conn)
return err
}
glog.V(LDEBUG).Infof("[socks5-bind] %s -> %s\n%s", conn.RemoteAddr(), req.Addr, req)
/*
// first reply
rep, err := peekReply(conn, fconn)
if err != nil {
glog.V(LWARNING).Infoln("[socks5] BIND forward", err)
return err
}
glog.V(LINFO).Infoln("[socks5] BIND forward on", rep.Addr, "OK")
// second reply
rep, err = peekReply(conn, fconn)
if err != nil {
glog.V(LWARNING).Infoln("[socks5] BIND forward accept", err)
return err
}
glog.V(LINFO).Infoln("[socks5] BIND forward accept", rep.Addr)
*/
glog.V(LINFO).Infof("[socks5-bind] %s <-> %s", conn.RemoteAddr(), req.Addr)
defer glog.V(LINFO).Infof("[socks5-bind] %s >-< %s", conn.RemoteAddr(), req.Addr)
return Transport(conn, fconn)
}
/*
func peekReply(dst, src net.Conn) (rep *gosocks5.Reply, err error) {
rep, err = gosocks5.ReadReply(src)
if err != nil {
glog.V(LWARNING).Infof("[socks5-bind] FORWARD %s <- : %s", dst.RemoteAddr(), err)
rep = gosocks5.NewReply(gosocks5.Failure, nil)
}
if err = rep.Write(dst); err != nil {
return
}
glog.V(LDEBUG).Infof("[socks5-bind] FORWARD %s <-\n%s", dst.RemoteAddr(), rep)
if rep.Rep != gosocks5.Succeeded {
err = errors.New("Failure")
}
return
} }
*/
func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) { func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) {
if len(forwardArgs) == 0 { if len(forwardArgs) == 0 {
...@@ -478,9 +448,6 @@ func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) { ...@@ -478,9 +448,6 @@ func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) {
fconn, _, err := forwardChain(forwardArgs...) fconn, _, err := forwardChain(forwardArgs...)
if err != nil { if err != nil {
if fconn != nil {
fconn.Close()
}
return return
} }
glog.V(LINFO).Infoln("[udp] forward associate") glog.V(LINFO).Infoln("[udp] forward associate")
...@@ -509,7 +476,7 @@ func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) { ...@@ -509,7 +476,7 @@ func createServerConn(uconn *net.UDPConn) (c *UDPConn, err error) {
} }
func ToSocksAddr(addr net.Addr) *gosocks5.Addr { func ToSocksAddr(addr net.Addr) *gosocks5.Addr {
host := "" host := "0.0.0.0"
port := 0 port := 0
if addr != nil { if addr != nil {
h, p, _ := net.SplitHostPort(addr.String()) h, p, _ := net.SplitHostPort(addr.String())
......
...@@ -25,8 +25,7 @@ type Args struct { ...@@ -25,8 +25,7 @@ type Args struct {
Addr string // host:port Addr string // host:port
Protocol string // protocol: http/socks(5)/ss Protocol string // protocol: http/socks(5)/ss
Transport string // transport: ws(s)/tls/tcp/udp/rtcp/rudp Transport string // transport: ws(s)/tls/tcp/udp/rtcp/rudp
Forward string // forward address, used by local tcp/udp port forwarding Remote string // remote address, used by tcp/udp port forwarding
Bind string // remote binding port, used by remote tcp/udp port forwarding
User *url.Userinfo // authentication for proxy User *url.Userinfo // authentication for proxy
Cert tls.Certificate // tls certificate Cert tls.Certificate // tls certificate
} }
...@@ -37,8 +36,8 @@ func (args Args) String() string { ...@@ -37,8 +36,8 @@ func (args Args) String() string {
authUser = args.User.Username() authUser = args.User.Username()
authPass, _ = args.User.Password() authPass, _ = args.User.Password()
} }
return fmt.Sprintf("host: %s, protocol: %s, transport: %s, forward: %s, auth: %s/%s", return fmt.Sprintf("host: %s, protocol: %s, transport: %s, remote: %s, auth: %s/%s",
args.Addr, args.Protocol, args.Transport, args.Forward, authUser, authPass) args.Addr, args.Protocol, args.Transport, args.Remote, authUser, authPass)
} }
func parseArgs(ss []string) (args []Args) { func parseArgs(ss []string) (args []Args) {
...@@ -77,13 +76,9 @@ func parseArgs(ss []string) (args []Args) { ...@@ -77,13 +76,9 @@ func parseArgs(ss []string) (args []Args) {
switch arg.Transport { switch arg.Transport {
case "ws", "wss", "tls": case "ws", "wss", "tls":
case "tcp", "udp": // started from v2.1, tcp and udp are for local port forwarding case "tcp", "udp": // started from v2.1, tcp and udp are for local port forwarding
arg.Forward = strings.Trim(u.EscapedPath(), "/") arg.Remote = strings.Trim(u.EscapedPath(), "/")
case "rtcp", "rudp": // started from v2.1, rtcp and rudp are for remote port forwarding\ case "rtcp", "rudp": // started from v2.1, rtcp and rudp are for remote port forwarding
if a := strings.Split(strings.Trim(u.EscapedPath(), "/"), ":"); len(a) == 3 { arg.Remote = strings.Trim(u.EscapedPath(), "/")
arg.Forward = a[0] + ":" + a[1]
arg.Bind = ":" + a[2]
glog.V(LINFO).Infoln(arg.Forward, arg.Bind)
}
default: default:
arg.Transport = "" arg.Transport = ""
} }
......
...@@ -112,12 +112,13 @@ func NewWs(arg Args) *ws { ...@@ -112,12 +112,13 @@ func NewWs(arg Args) *ws {
} }
func (s *ws) handle(w http.ResponseWriter, r *http.Request) { func (s *ws) handle(w http.ResponseWriter, r *http.Request) {
glog.V(LINFO).Infoln("[ws] %s - %s", r.RemoteAddr, s.arg.Addr)
if glog.V(LDEBUG) { if glog.V(LDEBUG) {
dump, err := httputil.DumpRequest(r, false) dump, err := httputil.DumpRequest(r, false)
if err != nil { if err != nil {
glog.Infoln(err) glog.V(LWARNING).Infoln("[ws] %s - %s : %s", r.RemoteAddr, s.arg.Addr, err)
} else { } else {
glog.Infoln(string(dump)) glog.V(LDEBUG).Infoln("[ws] %s - %s\n%s", r.RemoteAddr, s.arg.Addr, string(dump))
} }
} }
conn, err := s.upgrader.Upgrade(w, r, nil) conn, err := s.upgrader.Upgrade(w, r, nil)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment