mirror of https://github.com/ginuerzh/gost
15 changed files with 694 additions and 15 deletions
@ -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. |
||||
@ -0,0 +1,2 @@ |
|||||
|
# pht |
||||
|
Pure HTTP Tunnel - Tunnel over HTTP using only GET and POST requests. |
||||
@ -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 |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
Loading…
Reference in new issue