mirror of https://github.com/fatedier/frp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
5.3 KiB
244 lines
5.3 KiB
// Copyright 2017 frp team |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
|
|
package plugin |
|
|
|
import ( |
|
"bufio" |
|
"encoding/base64" |
|
"io" |
|
"net" |
|
"net/http" |
|
"strings" |
|
|
|
frpNet "github.com/fatedier/frp/pkg/util/net" |
|
|
|
frpIo "github.com/fatedier/golib/io" |
|
gnet "github.com/fatedier/golib/net" |
|
) |
|
|
|
const PluginHTTPProxy = "http_proxy" |
|
|
|
func init() { |
|
Register(PluginHTTPProxy, NewHTTPProxyPlugin) |
|
} |
|
|
|
type HTTPProxy struct { |
|
l *Listener |
|
s *http.Server |
|
AuthUser string |
|
AuthPasswd string |
|
} |
|
|
|
func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) { |
|
user := params["plugin_http_user"] |
|
passwd := params["plugin_http_passwd"] |
|
listener := NewProxyListener() |
|
|
|
hp := &HTTPProxy{ |
|
l: listener, |
|
AuthUser: user, |
|
AuthPasswd: passwd, |
|
} |
|
|
|
hp.s = &http.Server{ |
|
Handler: hp, |
|
} |
|
|
|
go hp.s.Serve(listener) |
|
return hp, nil |
|
} |
|
|
|
func (hp *HTTPProxy) Name() string { |
|
return PluginHTTPProxy |
|
} |
|
|
|
func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { |
|
wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) |
|
|
|
sc, rd := gnet.NewSharedConn(wrapConn) |
|
firstBytes := make([]byte, 7) |
|
_, err := rd.Read(firstBytes) |
|
if err != nil { |
|
wrapConn.Close() |
|
return |
|
} |
|
|
|
if strings.ToUpper(string(firstBytes)) == "CONNECT" { |
|
bufRd := bufio.NewReader(sc) |
|
request, err := http.ReadRequest(bufRd) |
|
if err != nil { |
|
wrapConn.Close() |
|
return |
|
} |
|
hp.handleConnectReq(request, frpIo.WrapReadWriteCloser(bufRd, wrapConn, wrapConn.Close)) |
|
return |
|
} |
|
|
|
hp.l.PutConn(sc) |
|
return |
|
} |
|
|
|
func (hp *HTTPProxy) Close() error { |
|
hp.s.Close() |
|
hp.l.Close() |
|
return nil |
|
} |
|
|
|
func (hp *HTTPProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { |
|
if ok := hp.Auth(req); !ok { |
|
rw.Header().Set("Proxy-Authenticate", "Basic") |
|
rw.WriteHeader(http.StatusProxyAuthRequired) |
|
return |
|
} |
|
|
|
if req.Method == http.MethodConnect { |
|
// deprecated |
|
// Connect request is handled in Handle function. |
|
hp.ConnectHandler(rw, req) |
|
} else { |
|
hp.HTTPHandler(rw, req) |
|
} |
|
} |
|
|
|
func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) { |
|
removeProxyHeaders(req) |
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req) |
|
if err != nil { |
|
http.Error(rw, err.Error(), http.StatusInternalServerError) |
|
return |
|
} |
|
defer resp.Body.Close() |
|
|
|
copyHeaders(rw.Header(), resp.Header) |
|
rw.WriteHeader(resp.StatusCode) |
|
|
|
_, err = io.Copy(rw, resp.Body) |
|
if err != nil && err != io.EOF { |
|
return |
|
} |
|
} |
|
|
|
// deprecated |
|
// Hijack needs to SetReadDeadline on the Conn of the request, but if we use stream compression here, |
|
// we may always get i/o timeout error. |
|
func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) { |
|
hj, ok := rw.(http.Hijacker) |
|
if !ok { |
|
rw.WriteHeader(http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
client, _, err := hj.Hijack() |
|
if err != nil { |
|
rw.WriteHeader(http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
remote, err := net.Dial("tcp", req.URL.Host) |
|
if err != nil { |
|
http.Error(rw, "Failed", http.StatusBadRequest) |
|
client.Close() |
|
return |
|
} |
|
client.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) |
|
|
|
go frpIo.Join(remote, client) |
|
} |
|
|
|
func (hp *HTTPProxy) Auth(req *http.Request) bool { |
|
if hp.AuthUser == "" && hp.AuthPasswd == "" { |
|
return true |
|
} |
|
|
|
s := strings.SplitN(req.Header.Get("Proxy-Authorization"), " ", 2) |
|
if len(s) != 2 { |
|
return false |
|
} |
|
|
|
b, err := base64.StdEncoding.DecodeString(s[1]) |
|
if err != nil { |
|
return false |
|
} |
|
|
|
pair := strings.SplitN(string(b), ":", 2) |
|
if len(pair) != 2 { |
|
return false |
|
} |
|
|
|
if pair[0] != hp.AuthUser || pair[1] != hp.AuthPasswd { |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
func (hp *HTTPProxy) handleConnectReq(req *http.Request, rwc io.ReadWriteCloser) { |
|
defer rwc.Close() |
|
if ok := hp.Auth(req); !ok { |
|
res := getBadResponse() |
|
res.Write(rwc) |
|
return |
|
} |
|
|
|
remote, err := net.Dial("tcp", req.URL.Host) |
|
if err != nil { |
|
res := &http.Response{ |
|
StatusCode: 400, |
|
Proto: "HTTP/1.1", |
|
ProtoMajor: 1, |
|
ProtoMinor: 1, |
|
} |
|
res.Write(rwc) |
|
return |
|
} |
|
rwc.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) |
|
|
|
frpIo.Join(remote, rwc) |
|
} |
|
|
|
func copyHeaders(dst, src http.Header) { |
|
for key, values := range src { |
|
for _, value := range values { |
|
dst.Add(key, value) |
|
} |
|
} |
|
} |
|
|
|
func removeProxyHeaders(req *http.Request) { |
|
req.RequestURI = "" |
|
req.Header.Del("Proxy-Connection") |
|
req.Header.Del("Connection") |
|
req.Header.Del("Proxy-Authenticate") |
|
req.Header.Del("Proxy-Authorization") |
|
req.Header.Del("TE") |
|
req.Header.Del("Trailers") |
|
req.Header.Del("Transfer-Encoding") |
|
req.Header.Del("Upgrade") |
|
} |
|
|
|
func getBadResponse() *http.Response { |
|
header := make(map[string][]string) |
|
header["Proxy-Authenticate"] = []string{"Basic"} |
|
header["Connection"] = []string{"close"} |
|
res := &http.Response{ |
|
Status: "407 Not authorized", |
|
StatusCode: 407, |
|
Proto: "HTTP/1.1", |
|
ProtoMajor: 1, |
|
ProtoMinor: 1, |
|
Header: header, |
|
} |
|
return res |
|
}
|
|
|