#76 support pure HTTP tunnel(PHT)

This commit is contained in:
rui.zheng 2017-02-05 14:35:38 +08:00
parent 72d8a598d5
commit 333291e9bc
15 changed files with 694 additions and 15 deletions

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
"github.com/ginuerzh/pht"
"github.com/golang/glog" "github.com/golang/glog"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"io" "io"
@ -29,6 +30,7 @@ type ProxyChain struct {
kcpConfig *KCPConfig kcpConfig *KCPConfig
kcpSession *KCPSession kcpSession *KCPSession
kcpMutex sync.Mutex kcpMutex sync.Mutex
phttpClient *pht.Client
} }
func NewProxyChain(nodes ...ProxyNode) *ProxyChain { func NewProxyChain(nodes ...ProxyNode) *ProxyChain {
@ -96,8 +98,8 @@ func (c *ProxyChain) Init() {
} }
for i, node := range c.nodes { for i, node := range c.nodes {
if node.Transport == "kcp" && i > 0 { if (node.Transport == "kcp" || node.Transport == "pht" || node.Transport == "quic") && i > 0 {
glog.Fatal("KCP must be the first node in the proxy chain") glog.Fatal("KCP/PHT/QUIC must be the first node in the proxy chain")
} }
} }
@ -118,6 +120,11 @@ func (c *ProxyChain) Init() {
c.kcpConfig = config c.kcpConfig = config
return return
} }
if c.nodes[0].Transport == "pht" {
glog.V(LINFO).Infoln("Pure HTTP mode is enabled")
c.phttpClient = pht.NewClient(c.nodes[0].Addr, c.nodes[0].Get("key"))
}
} }
func (c *ProxyChain) KCPEnabled() bool { func (c *ProxyChain) KCPEnabled() bool {
@ -269,6 +276,8 @@ func (c *ProxyChain) travelNodes(withHttp2 bool, nodes ...ProxyNode) (conn *Prox
cc, err = c.http2Connect(node.Addr) cc, err = c.http2Connect(node.Addr)
} else if node.Transport == "kcp" { } else if node.Transport == "kcp" {
cc, err = c.getKCPConn() cc, err = c.getKCPConn()
} else if node.Transport == "pht" {
cc, err = c.phttpClient.Dial()
} else { } else {
cc, err = net.DialTimeout("tcp", node.Addr, DialTimeout) cc, err = net.DialTimeout("tcp", node.Addr, DialTimeout)
} }

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
"github.com/ginuerzh/pht"
"github.com/golang/glog" "github.com/golang/glog"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"io" "io"
@ -29,6 +30,7 @@ type ProxyChain struct {
kcpConfig *KCPConfig kcpConfig *KCPConfig
kcpSession *KCPSession kcpSession *KCPSession
kcpMutex sync.Mutex kcpMutex sync.Mutex
phttpClient *pht.Client
} }
func NewProxyChain(nodes ...ProxyNode) *ProxyChain { func NewProxyChain(nodes ...ProxyNode) *ProxyChain {
@ -96,8 +98,8 @@ func (c *ProxyChain) Init() {
} }
for i, node := range c.nodes { for i, node := range c.nodes {
if node.Transport == "kcp" && i > 0 { if (node.Transport == "kcp" || node.Transport == "pht" || node.Transport == "quic") && i > 0 {
glog.Fatal("KCP must be the first node in the proxy chain") glog.Fatal("KCP/PHT/QUIC must be the first node in the proxy chain")
} }
} }
@ -118,6 +120,11 @@ func (c *ProxyChain) Init() {
c.kcpConfig = config c.kcpConfig = config
return return
} }
if c.nodes[0].Transport == "pht" {
glog.V(LINFO).Infoln("Pure HTTP mode is enabled")
c.phttpClient = pht.NewClient(c.nodes[0].Addr, c.nodes[0].Get("key"))
}
} }
func (c *ProxyChain) KCPEnabled() bool { func (c *ProxyChain) KCPEnabled() bool {
@ -269,6 +276,8 @@ func (c *ProxyChain) travelNodes(withHttp2 bool, nodes ...ProxyNode) (conn *Prox
cc, err = c.http2Connect(node.Addr) cc, err = c.http2Connect(node.Addr)
} else if node.Transport == "kcp" { } else if node.Transport == "kcp" {
cc, err = c.getKCPConn() cc, err = c.getKCPConn()
} else if node.Transport == "pht" {
cc, err = c.phttpClient.Dial()
} else { } else {
cc, err = net.DialTimeout("tcp", node.Addr, DialTimeout) cc, err = net.DialTimeout("tcp", node.Addr, DialTimeout)
} }

View File

@ -4,14 +4,14 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors"
"github.com/ginuerzh/pht"
"github.com/golang/glog" "github.com/golang/glog"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
//"strings"
"errors"
"time" "time"
) )
@ -383,3 +383,30 @@ func (fw flushWriter) Write(p []byte) (n int, err error) {
} }
return return
} }
type PureHttpServer struct {
Base *ProxyServer
Handler func(net.Conn)
}
func NewPureHttpServer(base *ProxyServer) *PureHttpServer {
return &PureHttpServer{
Base: base,
}
}
func (s *PureHttpServer) ListenAndServe() error {
server := pht.Server{
Addr: s.Base.Node.Addr,
Key: s.Base.Node.Get("key"),
}
if server.Handler == nil {
server.Handler = s.handleConn
}
return server.ListenAndServe()
}
func (s *PureHttpServer) handleConn(conn net.Conn) {
glog.V(LINFO).Infof("[pht] %s - %s", conn.RemoteAddr(), conn.LocalAddr())
s.Base.handleConn(conn)
}

View File

@ -71,7 +71,7 @@ func ParseProxyNode(s string) (node ProxyNode, err error) {
} }
switch node.Transport { switch node.Transport {
case "ws", "wss", "tls", "http2", "quic", "kcp", "redirect", "ssu": case "ws", "wss", "tls", "http2", "quic", "kcp", "redirect", "ssu", "pht":
case "https": case "https":
node.Protocol = "http" node.Protocol = "http"
node.Transport = "tls" node.Transport = "tls"

View File

@ -122,6 +122,8 @@ func (s *ProxyServer) Serve() error {
ttl = DefaultTTL ttl = DefaultTTL
} }
return NewShadowUdpServer(s, ttl).ListenAndServe() return NewShadowUdpServer(s, ttl).ListenAndServe()
case "pht": // pure http tunnel
return NewPureHttpServer(s).ListenAndServe()
default: default:
ln, err = net.Listen("tcp", node.Addr) ln, err = net.Listen("tcp", node.Addr)
} }

21
cmd/gost/vendor/github.com/ginuerzh/pht/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 ginuerzh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
cmd/gost/vendor/github.com/ginuerzh/pht/README.md generated vendored Normal file
View File

@ -0,0 +1,2 @@
# pht
Pure HTTP Tunnel - Tunnel over HTTP using only GET and POST requests.

162
cmd/gost/vendor/github.com/ginuerzh/pht/client.go generated vendored Normal file
View File

@ -0,0 +1,162 @@
package pht
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
)
type Client struct {
Host string
Key string
httpClient *http.Client
manager *sessionManager
}
func NewClient(host, key string) *Client {
return &Client{
Host: host,
Key: key,
httpClient: &http.Client{},
manager: newSessionManager(),
}
}
func (c *Client) Dial() (net.Conn, error) {
r, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s%s", c.Host, tokenURI), nil)
if err != nil {
return nil, err
}
r.Header.Set("Authorization", "key="+c.Key)
resp, err := c.httpClient.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(resp.Status)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
token := strings.TrimPrefix(string(data), "token=")
if token == "" {
return nil, errors.New("invalid token")
}
session := newSession(0, 0)
c.manager.SetSession(token, session)
go c.sendDataLoop(token)
go c.recvDataLoop(token)
return newConn(session), nil
}
func (c *Client) sendDataLoop(token string) error {
session := c.manager.GetSession(token)
if session == nil {
return errors.New("invalid token")
}
for {
select {
case b, ok := <-session.wchan:
var data string
if len(b) > 0 {
data = base64.StdEncoding.EncodeToString(b)
}
r, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s%s", c.Host, pushURI), bytes.NewBufferString(data+"\n"))
if err != nil {
return err
}
r.Header.Set("Authorization", fmt.Sprintf("key=%s; token=%s", c.Key, token))
if !ok {
c.manager.DelSession(token)
resp, err := c.httpClient.Do(r)
if err != nil { // TODO: retry
return err
}
resp.Body.Close()
return nil // session is closed
}
resp, err := c.httpClient.Do(r)
if err != nil { // TODO: retry
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
}
}
}
func (c *Client) recvDataLoop(token string) error {
session := c.manager.GetSession(token)
if session == nil {
return errors.New("invalid token")
}
for {
err := c.recvData(token, session)
if err != nil {
close(session.rchan)
c.manager.DelSession(token)
return err
}
}
}
func (c *Client) recvData(token string, s *session) error {
r, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", c.Host, pollURI), nil)
if err != nil {
return err
}
r.Header.Set("Authorization", fmt.Sprintf("key=%s; token=%s", c.Key, token))
resp, err := c.httpClient.Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
select {
case <-s.closed:
return errors.New("session closed")
default:
}
b, err := base64.StdEncoding.DecodeString(scanner.Text())
if err != nil {
return err
}
select {
case s.rchan <- b:
case <-s.closed:
return errors.New("session closed")
case <-time.After(time.Second * 90):
return errors.New("timeout")
}
if err := scanner.Err(); err != nil {
return err
}
}
return nil
}

133
cmd/gost/vendor/github.com/ginuerzh/pht/conn.go generated vendored Normal file
View File

@ -0,0 +1,133 @@
package pht
import (
"errors"
"io"
"net"
"time"
)
type conn struct {
session *session
rb []byte // read buffer
remoteAddr net.Addr
localAddr net.Addr
rTimer, wTimer *time.Timer
closed chan interface{}
}
func newConn(session *session) *conn {
conn := &conn{
session: session,
rTimer: time.NewTimer(time.Hour * 65535),
wTimer: time.NewTimer(time.Hour * 65535),
closed: make(chan interface{}),
}
conn.rTimer.Stop()
conn.wTimer.Stop()
return conn
}
func (conn *conn) Read(b []byte) (n int, err error) {
select {
case <-conn.closed:
err = errors.New("read: use of closed network connection")
return
default:
}
if len(conn.rb) > 0 {
n = copy(b, conn.rb)
conn.rb = conn.rb[n:]
return
}
select {
case data, ok := <-conn.session.rchan:
if !ok {
err = io.EOF
return
}
n = copy(b, data)
conn.rb = data[n:]
case <-conn.rTimer.C:
err = errors.New("read timeout")
case <-conn.closed:
err = io.EOF
}
return
}
func (conn *conn) Write(b []byte) (n int, err error) {
select {
case <-conn.closed:
err = errors.New("write: use of closed network connection")
return
default:
}
if len(b) == 0 {
return
}
data := make([]byte, len(b))
copy(data, b)
select {
case conn.session.wchan <- data:
n = len(b)
case <-conn.wTimer.C:
err = errors.New("write timeout")
case <-conn.closed:
err = errors.New("connection is closed")
}
return
}
func (conn *conn) Close() error {
close(conn.closed)
close(conn.session.closed)
close(conn.session.wchan)
return nil
}
func (conn *conn) LocalAddr() net.Addr {
return conn.localAddr
}
func (conn *conn) RemoteAddr() net.Addr {
return conn.remoteAddr
}
func (conn *conn) SetReadDeadline(t time.Time) error {
if t.IsZero() {
conn.rTimer.Stop()
return nil
}
conn.rTimer.Reset(t.Sub(time.Now()))
return nil
}
func (conn *conn) SetWriteDeadline(t time.Time) error {
if t.IsZero() {
conn.wTimer.Stop()
return nil
}
conn.wTimer.Reset(t.Sub(time.Now()))
return nil
}
func (conn *conn) SetDeadline(t time.Time) error {
if t.IsZero() {
conn.rTimer.Stop()
conn.wTimer.Stop()
return nil
}
d := t.Sub(time.Now())
conn.rTimer.Reset(d)
conn.wTimer.Reset(d)
return nil
}

199
cmd/gost/vendor/github.com/ginuerzh/pht/server.go generated vendored Normal file
View File

@ -0,0 +1,199 @@
package pht
import (
"bufio"
"encoding/base64"
"fmt"
"net"
"net/http"
"strings"
"time"
)
const (
tokenURI = "/token"
pushURI = "/push"
pollURI = "/poll"
)
type Server struct {
Addr string
Key string
Handler func(net.Conn)
manager *sessionManager
}
func (s *Server) ListenAndServe() error {
s.manager = newSessionManager()
mux := http.NewServeMux()
mux.Handle(tokenURI, http.HandlerFunc(s.tokenHandler))
mux.Handle(pushURI, http.HandlerFunc(s.pushHandler))
mux.Handle(pollURI, http.HandlerFunc(s.pollHandler))
return http.ListenAndServe(s.Addr, mux)
}
func (s *Server) tokenHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
m := parseAuth(r.Header.Get("Authorization"))
if m["key"] != s.Key {
w.WriteHeader(http.StatusForbidden)
return
}
token, session, err := s.manager.NewSession(0, 0)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
conn, err := s.upgrade(session, r)
if err != nil {
s.manager.DelSession(token)
w.WriteHeader(http.StatusInternalServerError)
return
}
if s.Handler != nil {
go s.Handler(conn)
}
w.Write([]byte(fmt.Sprintf("token=%s", token)))
}
func (s *Server) pushHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
m := parseAuth(r.Header.Get("Authorization"))
if m["key"] != s.Key {
w.WriteHeader(http.StatusForbidden)
return
}
token := m["token"]
session := s.manager.GetSession(token)
if session == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
br := bufio.NewReader(r.Body)
data, err := br.ReadString('\n')
if err != nil {
s.manager.DelSession(token)
close(session.rchan)
w.WriteHeader(http.StatusInternalServerError)
return
}
data = strings.TrimSuffix(data, "\n")
if len(data) == 0 {
s.manager.DelSession(token)
close(session.rchan)
return
}
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
s.manager.DelSession(token)
close(session.rchan)
return
}
select {
case <-session.closed:
s.manager.DelSession(token)
return
case session.rchan <- b:
w.WriteHeader(http.StatusOK)
case <-time.After(time.Second * 90):
s.manager.DelSession(token)
w.WriteHeader(http.StatusRequestTimeout)
}
}
func (s *Server) pollHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
m := parseAuth(r.Header.Get("Authorization"))
if m["key"] != s.Key {
w.WriteHeader(http.StatusForbidden)
return
}
token := m["token"]
session := s.manager.GetSession(token)
if session == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
if fw, ok := w.(http.Flusher); ok {
fw.Flush()
}
for {
select {
case data, ok := <-session.wchan:
if !ok {
s.manager.DelSession(token)
return // session is closed
}
bw := bufio.NewWriter(w)
bw.WriteString(base64.StdEncoding.EncodeToString(data))
bw.WriteString("\n")
if err := bw.Flush(); err != nil {
return
}
if fw, ok := w.(http.Flusher); ok {
fw.Flush()
}
case <-time.After(time.Second * 25):
return
}
}
}
func (s *Server) upgrade(sess *session, r *http.Request) (net.Conn, error) {
conn := newConn(sess)
raddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err != nil {
raddr = &net.TCPAddr{}
}
conn.remoteAddr = raddr
laddr, err := net.ResolveTCPAddr("tcp", s.Addr)
if err != nil {
laddr = &net.TCPAddr{}
}
conn.localAddr = laddr
return conn, nil
}
func parseAuth(auth string) map[string]string {
mkv := make(map[string]string)
for _, s := range strings.Split(auth, ";") {
n := strings.Index(s, "=")
if n < 0 {
continue
}
mkv[strings.TrimSpace(s[:n])] = strings.TrimSpace(s[n+1:])
}
return mkv
}

80
cmd/gost/vendor/github.com/ginuerzh/pht/session.go generated vendored Normal file
View File

@ -0,0 +1,80 @@
package pht
import (
"crypto/rand"
"encoding/hex"
"sync"
)
const (
defaultRChanLen = 64
defaultWChanLen = 64
)
type session struct {
rchan chan []byte
wchan chan []byte
closed chan interface{}
}
func newSession(rlen, wlen int) *session {
if rlen <= 0 {
rlen = defaultRChanLen
}
if wlen <= 0 {
wlen = defaultWChanLen
}
return &session{
rchan: make(chan []byte, rlen),
wchan: make(chan []byte, wlen),
closed: make(chan interface{}),
}
}
type sessionManager struct {
sessions map[string]*session
mux sync.Mutex
}
func newSessionManager() *sessionManager {
return &sessionManager{
sessions: make(map[string]*session),
mux: sync.Mutex{},
}
}
func (m *sessionManager) NewSession(rlen, wlen int) (token string, s *session, err error) {
var nonce [16]byte
if _, err = rand.Read(nonce[:]); err != nil {
return
}
token = hex.EncodeToString(nonce[:])
s = newSession(rlen, wlen)
m.mux.Lock()
defer m.mux.Unlock()
m.sessions[token] = s
return
}
func (m *sessionManager) SetSession(token string, session *session) {
m.mux.Lock()
defer m.mux.Unlock()
m.sessions[token] = session
}
func (m *sessionManager) GetSession(token string) *session {
m.mux.Lock()
defer m.mux.Unlock()
return m.sessions[token]
}
func (m *sessionManager) DelSession(token string) {
m.mux.Lock()
defer m.mux.Unlock()
delete(m.sessions, token)
}

View File

@ -21,10 +21,16 @@
"revisionTime": "2017-01-19T05:34:58Z" "revisionTime": "2017-01-19T05:34:58Z"
}, },
{ {
"checksumSHA1": "VXFPGZtx+GKexkXeL3YljqJLHFk=", "checksumSHA1": "ero0DQrGYph2eFDyEjDnOQV8NHo=",
"path": "github.com/ginuerzh/gost", "path": "github.com/ginuerzh/gost",
"revision": "dc75931858cf96d95525faa22b7ffa0cea3e7d68", "revision": "72d8a598d50f8517d922f233e8f8d37011fcb18f",
"revisionTime": "2017-01-25T04:21:04Z" "revisionTime": "2017-01-25T04:23:26Z"
},
{
"checksumSHA1": "+XIOnTW0rv8Kr/amkXgMraNeUr4=",
"path": "github.com/ginuerzh/pht",
"revision": "f90acb425616767b84789d10e50a6fb652cd1e9a",
"revisionTime": "2017-02-05T06:05:58Z"
}, },
{ {
"checksumSHA1": "URsJa4y/sUUw/STmbeYx9EKqaYE=", "checksumSHA1": "URsJa4y/sUUw/STmbeYx9EKqaYE=",

31
http.go
View File

@ -4,14 +4,14 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors"
"github.com/ginuerzh/pht"
"github.com/golang/glog" "github.com/golang/glog"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
//"strings"
"errors"
"time" "time"
) )
@ -383,3 +383,30 @@ func (fw flushWriter) Write(p []byte) (n int, err error) {
} }
return return
} }
type PureHttpServer struct {
Base *ProxyServer
Handler func(net.Conn)
}
func NewPureHttpServer(base *ProxyServer) *PureHttpServer {
return &PureHttpServer{
Base: base,
}
}
func (s *PureHttpServer) ListenAndServe() error {
server := pht.Server{
Addr: s.Base.Node.Addr,
Key: s.Base.Node.Get("key"),
}
if server.Handler == nil {
server.Handler = s.handleConn
}
return server.ListenAndServe()
}
func (s *PureHttpServer) handleConn(conn net.Conn) {
glog.V(LINFO).Infof("[pht] %s - %s", conn.RemoteAddr(), conn.LocalAddr())
s.Base.handleConn(conn)
}

View File

@ -71,7 +71,7 @@ func ParseProxyNode(s string) (node ProxyNode, err error) {
} }
switch node.Transport { switch node.Transport {
case "ws", "wss", "tls", "http2", "quic", "kcp", "redirect", "ssu": case "ws", "wss", "tls", "http2", "quic", "kcp", "redirect", "ssu", "pht":
case "https": case "https":
node.Protocol = "http" node.Protocol = "http"
node.Transport = "tls" node.Transport = "tls"

View File

@ -122,6 +122,8 @@ func (s *ProxyServer) Serve() error {
ttl = DefaultTTL ttl = DefaultTTL
} }
return NewShadowUdpServer(s, ttl).ListenAndServe() return NewShadowUdpServer(s, ttl).ListenAndServe()
case "pht": // pure http tunnel
return NewPureHttpServer(s).ListenAndServe()
default: default:
ln, err = net.Listen("tcp", node.Addr) ln, err = net.Listen("tcp", node.Addr)
} }