add HTTP2 proxy client support
This commit is contained in:
parent
36b61e0580
commit
c02bc32cb6
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ginuerzh/gost/gost"
|
"github.com/ginuerzh/gost/gost"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -25,6 +26,7 @@ func init() {
|
|||||||
flag.IntVar(&requests, "n", 1, "Number of requests to perform")
|
flag.IntVar(&requests, "n", 1, "Number of requests to perform")
|
||||||
flag.IntVar(&concurrency, "c", 1, "Number of multiple requests to make at a time")
|
flag.IntVar(&concurrency, "c", 1, "Number of multiple requests to make at a time")
|
||||||
flag.BoolVar(&quiet, "q", false, "quiet mode")
|
flag.BoolVar(&quiet, "q", false, "quiet mode")
|
||||||
|
flag.BoolVar(&http2.VerboseLogs, "v", false, "HTTP2 verbose logs")
|
||||||
flag.BoolVar(&gost.Debug, "d", false, "debug mode")
|
flag.BoolVar(&gost.Debug, "d", false, "debug mode")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -103,17 +105,13 @@ func main() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// http2+tls, http2+tcp
|
// http2
|
||||||
gost.Node{
|
gost.Node{
|
||||||
Addr: "127.0.0.1:1443",
|
Addr: "127.0.0.1:1443",
|
||||||
Client: gost.NewClient(
|
Client: &gost.Client{
|
||||||
gost.HTTP2Connector(url.UserPassword("admin", "123456")),
|
Connector: gost.HTTP2Connector(url.UserPassword("admin", "123456")),
|
||||||
gost.HTTP2Transporter(
|
Transporter: gost.HTTP2Transporter(nil),
|
||||||
nil,
|
},
|
||||||
&tls.Config{InsecureSkipVerify: true}, // or nil, will use h2c mode (http2+tcp).
|
|
||||||
time.Second*1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -149,13 +147,14 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
// socks5+h2
|
||||||
gost.Node{
|
gost.Node{
|
||||||
Addr: "localhost:8443",
|
Addr: "localhost:8443",
|
||||||
Client: &gost.Client{
|
Client: &gost.Client{
|
||||||
// Connector: gost.HTTPConnector(url.UserPassword("admin", "123456")),
|
// Connector: gost.HTTPConnector(url.UserPassword("admin", "123456")),
|
||||||
Connector: gost.SOCKS5Connector(url.UserPassword("admin", "123456")),
|
Connector: gost.SOCKS5Connector(url.UserPassword("admin", "123456")),
|
||||||
// Transporter: gost.H2CTransporter(), // HTTP2 h2c mode
|
// Transporter: gost.H2CTransporter(), // HTTP2 h2c mode
|
||||||
Transporter: gost.H2Transporter(), // HTTP2 h2
|
Transporter: gost.H2Transporter(nil), // HTTP2 h2
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -222,8 +222,7 @@ func http2Server() {
|
|||||||
// http2.VerboseLogs = true
|
// http2.VerboseLogs = true
|
||||||
|
|
||||||
s := &gost.Server{}
|
s := &gost.Server{}
|
||||||
ln, err := gost.TLSListener(":1443", tlsConfig()) // HTTP2 h2 mode
|
ln, err := gost.HTTP2Listener(":1443", tlsConfig())
|
||||||
// ln, err := gost.TCPListener(":1443") // HTTP2 h2c mode
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
243
gost/http2.go
243
gost/http2.go
@ -2,12 +2,10 @@ package gost
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -31,37 +29,36 @@ func HTTP2Connector(user *url.Userinfo) Connector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2Connector) Connect(conn net.Conn, addr string) (net.Conn, error) {
|
func (c *http2Connector) Connect(conn net.Conn, addr string) (net.Conn, error) {
|
||||||
cc, ok := conn.(*http2DummyConn)
|
cc, ok := conn.(*http2ClientConn)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("wrong connection type")
|
return nil, errors.New("wrong connection type")
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
u := &url.URL{
|
req := &http.Request{
|
||||||
Host: addr,
|
Method: http.MethodConnect,
|
||||||
}
|
URL: &url.URL{Scheme: "https", Host: addr},
|
||||||
req, err := http.NewRequest(http.MethodConnect, u.String(), ioutil.NopCloser(pr))
|
Header: make(http.Header),
|
||||||
if err != nil {
|
Proto: "HTTP/2.0",
|
||||||
log.Logf("[http2] %s - %s : %s", cc.raddr, addr, err)
|
ProtoMajor: 2,
|
||||||
return nil, err
|
ProtoMinor: 0,
|
||||||
|
Body: pr,
|
||||||
|
Host: addr,
|
||||||
|
ContentLength: -1,
|
||||||
}
|
}
|
||||||
|
// req.Header.Set("Gost-Target", addr) // Flag header to indicate the address that server connected to
|
||||||
if c.User != nil {
|
if c.User != nil {
|
||||||
req.Header.Set("Proxy-Authorization",
|
req.Header.Set("Proxy-Authorization",
|
||||||
"Basic "+base64.StdEncoding.EncodeToString([]byte(c.User.String())))
|
"Basic "+base64.StdEncoding.EncodeToString([]byte(c.User.String())))
|
||||||
}
|
}
|
||||||
req.ProtoMajor = 2
|
|
||||||
req.ProtoMinor = 0
|
|
||||||
|
|
||||||
if Debug {
|
if Debug {
|
||||||
dump, _ := httputil.DumpRequest(req, false)
|
dump, _ := httputil.DumpRequest(req, false)
|
||||||
log.Log("[http2]", string(dump))
|
log.Log("[http2]", string(dump))
|
||||||
}
|
}
|
||||||
resp, err := cc.conn.RoundTrip(req)
|
resp, err := cc.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Logf("[http2] %s - %s : %s", cc.raddr, addr, err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if Debug {
|
if Debug {
|
||||||
dump, _ := httputil.DumpResponse(resp, false)
|
dump, _ := httputil.DumpResponse(resp, false)
|
||||||
log.Log("[http2]", string(dump))
|
log.Log("[http2]", string(dump))
|
||||||
@ -71,72 +68,64 @@ func (c *http2Connector) Connect(conn net.Conn, addr string) (net.Conn, error) {
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil, errors.New(resp.Status)
|
return nil, errors.New(resp.Status)
|
||||||
}
|
}
|
||||||
hc := &http2Conn{r: resp.Body, w: pw}
|
hc := &http2Conn{
|
||||||
hc.remoteAddr, _ = net.ResolveTCPAddr("tcp", cc.raddr)
|
r: resp.Body,
|
||||||
|
w: pw,
|
||||||
|
closed: make(chan struct{}),
|
||||||
|
}
|
||||||
|
hc.remoteAddr, _ = net.ResolveTCPAddr("tcp", addr)
|
||||||
|
hc.localAddr, _ = net.ResolveTCPAddr("tcp", cc.addr)
|
||||||
|
|
||||||
return hc, nil
|
return hc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type http2Transporter struct {
|
type http2Transporter struct {
|
||||||
tlsConfig *tls.Config
|
clients map[string]*http.Client
|
||||||
tr *http2.Transport
|
clientMutex sync.Mutex
|
||||||
chain *Chain
|
tlsConfig *tls.Config
|
||||||
sessions map[string]*http2Session
|
|
||||||
sessionMutex sync.Mutex
|
|
||||||
pingInterval time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP2Transporter creates a Transporter that is used by HTTP2 proxy client.
|
// HTTP2Transporter creates a Transporter that is used by HTTP2 h2 proxy client.
|
||||||
//
|
func HTTP2Transporter(config *tls.Config) Transporter {
|
||||||
// Optional chain is a proxy chain that can be used to establish a connection with the HTTP2 server.
|
if config == nil {
|
||||||
//
|
config = &tls.Config{InsecureSkipVerify: true}
|
||||||
// Optional config is a TLS config for TLS handshake, if is nil, will use h2c mode.
|
}
|
||||||
//
|
|
||||||
// Optional ping is the ping interval, if is zero, ping will not be enabled.
|
|
||||||
func HTTP2Transporter(chain *Chain, config *tls.Config, ping time.Duration) Transporter {
|
|
||||||
return &http2Transporter{
|
return &http2Transporter{
|
||||||
tlsConfig: config,
|
clients: make(map[string]*http.Client),
|
||||||
tr: new(http2.Transport),
|
tlsConfig: config,
|
||||||
chain: chain,
|
|
||||||
pingInterval: ping,
|
|
||||||
sessions: make(map[string]*http2Session),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tr *http2Transporter) Dial(addr string, options ...DialOption) (net.Conn, error) {
|
func (tr *http2Transporter) Dial(addr string, options ...DialOption) (net.Conn, error) {
|
||||||
tr.sessionMutex.Lock()
|
opts := &DialOptions{}
|
||||||
defer tr.sessionMutex.Unlock()
|
for _, option := range options {
|
||||||
|
option(opts)
|
||||||
|
}
|
||||||
|
|
||||||
session, ok := tr.sessions[addr]
|
tr.clientMutex.Lock()
|
||||||
|
client, ok := tr.clients[addr]
|
||||||
if !ok {
|
if !ok {
|
||||||
conn, err := tr.chain.Dial(addr)
|
transport := http2.Transport{
|
||||||
if err != nil {
|
TLSClientConfig: tr.tlsConfig,
|
||||||
return nil, err
|
DialTLS: func(network, adr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
conn, err := opts.Chain.Dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wrapTLSClient(conn, cfg)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
client = &http.Client{
|
||||||
if tr.tlsConfig != nil {
|
Transport: &transport,
|
||||||
tc := tls.Client(conn, tr.tlsConfig)
|
Timeout: opts.Timeout,
|
||||||
if err := tc.Handshake(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn = tc
|
|
||||||
}
|
}
|
||||||
cc, err := tr.tr.NewClientConn(conn)
|
tr.clients[addr] = client
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
session = newHTTP2Session(conn, cc, tr.pingInterval)
|
|
||||||
tr.sessions[addr] = session
|
|
||||||
}
|
}
|
||||||
|
tr.clientMutex.Unlock()
|
||||||
|
|
||||||
if !session.Healthy() {
|
return &http2ClientConn{
|
||||||
session.Close()
|
addr: addr,
|
||||||
delete(tr.sessions, addr) // TODO: we could re-connect to the addr automatically.
|
client: client,
|
||||||
return nil, errors.New("connection is dead")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http2DummyConn{
|
|
||||||
raddr: addr,
|
|
||||||
conn: session.clientConn,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,13 +143,18 @@ type h2Transporter struct {
|
|||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func H2Transporter() Transporter {
|
// H2Transporter creates a Transporter that is used by HTTP2 h2 tunnel client.
|
||||||
|
func H2Transporter(config *tls.Config) Transporter {
|
||||||
|
if config == nil {
|
||||||
|
config = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
}
|
||||||
return &h2Transporter{
|
return &h2Transporter{
|
||||||
clients: make(map[string]*http.Client),
|
clients: make(map[string]*http.Client),
|
||||||
tlsConfig: &tls.Config{InsecureSkipVerify: true},
|
tlsConfig: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// H2CTransporter creates a Transporter that is used by HTTP2 h2c tunnel client.
|
||||||
func H2CTransporter() Transporter {
|
func H2CTransporter() Transporter {
|
||||||
return &h2Transporter{
|
return &h2Transporter{
|
||||||
clients: make(map[string]*http.Client),
|
clients: make(map[string]*http.Client),
|
||||||
@ -186,7 +180,7 @@ func (tr *h2Transporter) Dial(addr string, options ...DialOption) (net.Conn, err
|
|||||||
if tr.tlsConfig == nil {
|
if tr.tlsConfig == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
return wrapTLSClient(conn, tr.tlsConfig)
|
return wrapTLSClient(conn, cfg)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
client = &http.Client{
|
client = &http.Client{
|
||||||
@ -277,11 +271,10 @@ func (h *http2Handler) Handle(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *http2Handler) roundTrip(w http.ResponseWriter, r *http.Request) {
|
func (h *http2Handler) roundTrip(w http.ResponseWriter, r *http.Request) {
|
||||||
target := r.Header.Get("Gost-Target") // compitable with old version
|
target := r.Header.Get("Gost-Target")
|
||||||
if target == "" {
|
if target == "" {
|
||||||
target = r.Host
|
target = r.Host
|
||||||
}
|
}
|
||||||
// target := r.Host
|
|
||||||
if !strings.Contains(target, ":") {
|
if !strings.Contains(target, ":") {
|
||||||
target += ":80"
|
target += ":80"
|
||||||
}
|
}
|
||||||
@ -391,6 +384,7 @@ type http2Listener struct {
|
|||||||
errChan chan error
|
errChan chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP2Listener creates a Listener for HTTP2 proxy server.
|
||||||
func HTTP2Listener(addr string, config *tls.Config) (Listener, error) {
|
func HTTP2Listener(addr string, config *tls.Config) (Listener, error) {
|
||||||
l := &http2Listener{
|
l := &http2Listener{
|
||||||
connChan: make(chan *http2ServerConn, 1024),
|
connChan: make(chan *http2ServerConn, 1024),
|
||||||
@ -596,91 +590,6 @@ func (l *h2Listener) Accept() (conn net.Conn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type http2Session struct {
|
|
||||||
conn net.Conn
|
|
||||||
clientConn *http2.ClientConn
|
|
||||||
closeChan chan struct{}
|
|
||||||
pingChan chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP2Session(conn net.Conn, clientConn *http2.ClientConn, interval time.Duration) *http2Session {
|
|
||||||
session := &http2Session{
|
|
||||||
conn: conn,
|
|
||||||
clientConn: clientConn,
|
|
||||||
closeChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
if interval > 0 {
|
|
||||||
session.pingChan = make(chan struct{})
|
|
||||||
go session.Ping(interval)
|
|
||||||
}
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *http2Session) Ping(interval time.Duration) {
|
|
||||||
if interval <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer close(s.pingChan)
|
|
||||||
log.Log("[http2] ping is enabled, interval:", interval)
|
|
||||||
|
|
||||||
baseCtx := context.Background()
|
|
||||||
t := time.NewTicker(interval)
|
|
||||||
retries := PingRetries
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
if Debug {
|
|
||||||
log.Log("[http2] sending ping")
|
|
||||||
}
|
|
||||||
if !s.clientConn.CanTakeNewRequest() {
|
|
||||||
log.Logf("[http2] connection is dead")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(baseCtx, PingTimeout)
|
|
||||||
if err := s.clientConn.Ping(ctx); err != nil {
|
|
||||||
log.Logf("[http2] ping: %s", err)
|
|
||||||
if retries > 0 {
|
|
||||||
retries--
|
|
||||||
log.Log("[http2] retry ping")
|
|
||||||
cancel()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if Debug {
|
|
||||||
log.Log("[http2] ping OK")
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
retries = PingRetries
|
|
||||||
|
|
||||||
case <-s.closeChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *http2Session) Healthy() bool {
|
|
||||||
select {
|
|
||||||
case <-s.pingChan:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return s.clientConn.CanTakeNewRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *http2Session) Close() error {
|
|
||||||
select {
|
|
||||||
case <-s.closeChan:
|
|
||||||
default:
|
|
||||||
close(s.closeChan)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP2 connection, wrapped up just like a net.Conn
|
// HTTP2 connection, wrapped up just like a net.Conn
|
||||||
type http2Conn struct {
|
type http2Conn struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
@ -780,41 +689,41 @@ func (c *http2ServerConn) SetWriteDeadline(t time.Time) error {
|
|||||||
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy HTTP2 connection.
|
// a dummy HTTP2 client conn used by HTTP2 client connector
|
||||||
type http2DummyConn struct {
|
type http2ClientConn struct {
|
||||||
raddr string
|
addr string
|
||||||
conn *http2.ClientConn
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) Read(b []byte) (n int, err error) {
|
func (c *http2ClientConn) Read(b []byte) (n int, err error) {
|
||||||
return 0, &net.OpError{Op: "read", Net: "http2", Source: nil, Addr: nil, Err: errors.New("read not supported")}
|
return 0, &net.OpError{Op: "read", Net: "http2", Source: nil, Addr: nil, Err: errors.New("read not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) Write(b []byte) (n int, err error) {
|
func (c *http2ClientConn) Write(b []byte) (n int, err error) {
|
||||||
return 0, &net.OpError{Op: "write", Net: "http2", Source: nil, Addr: nil, Err: errors.New("write not supported")}
|
return 0, &net.OpError{Op: "write", Net: "http2", Source: nil, Addr: nil, Err: errors.New("write not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) Close() error {
|
func (c *http2ClientConn) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) LocalAddr() net.Addr {
|
func (c *http2ClientConn) LocalAddr() net.Addr {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) RemoteAddr() net.Addr {
|
func (c *http2ClientConn) RemoteAddr() net.Addr {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) SetDeadline(t time.Time) error {
|
func (c *http2ClientConn) SetDeadline(t time.Time) error {
|
||||||
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) SetReadDeadline(t time.Time) error {
|
func (c *http2ClientConn) SetReadDeadline(t time.Time) error {
|
||||||
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *http2DummyConn) SetWriteDeadline(t time.Time) error {
|
func (c *http2ClientConn) SetWriteDeadline(t time.Time) error {
|
||||||
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ func init() {
|
|||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogLogger uses the standard log package as the logger
|
||||||
type LogLogger struct {
|
type LogLogger struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ func (l *LogLogger) Logf(format string, v ...interface{}) {
|
|||||||
log.Output(3, fmt.Sprintf(format, v...))
|
log.Output(3, fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NopLogger is a null logger that discards the log outputs
|
||||||
type NopLogger struct {
|
type NopLogger struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user