|
|
|
package httpreverse
|
|
|
|
|
|
|
|
import (
|
|
|
|
"b612.me/apps/b612/httpreverse/rp"
|
|
|
|
"b612.me/starlog"
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var version = "2.1.0"
|
|
|
|
|
|
|
|
func (h *ReverseConfig) Run() error {
|
|
|
|
err := h.init()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for key, proxy := range h.proxy {
|
|
|
|
h.httpmux.HandleFunc(key, func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
starlog.Infof("<%s> Req Path:%s ListenAddr:%s UA:%s\n", h.Name, request.URL.Path, request.RemoteAddr, request.Header.Get("User-Agent"))
|
|
|
|
if !h.BasicAuth(writer, request) {
|
|
|
|
h.SetResponseHeader(writer)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !h.filter(writer, request) {
|
|
|
|
h.SetResponseHeader(writer)
|
|
|
|
writer.WriteHeader(403)
|
|
|
|
if len(h.warnpagedata) != 0 {
|
|
|
|
writer.Write(h.warnpagedata)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
writer.Write([]byte(`
|
|
|
|
<html>
|
|
|
|
<head><title>403 Forbidden</title></head>
|
|
|
|
<body>
|
|
|
|
<center><h1>403 Forbidden</h1></center>
|
|
|
|
<center><h3>You Are Not Allowed to Access This Page</h3></center>
|
|
|
|
<center><h3>Please Contact Site Administrator For Help</h3></center>
|
|
|
|
<hr><center>B612 HTTP REVERSE SERVER</center>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
proxy.ServeHTTP(writer, request)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
h.httpserver = http.Server{
|
|
|
|
Addr: fmt.Sprintf("%s:%d", h.Addr, h.Port),
|
|
|
|
Handler: &h.httpmux,
|
|
|
|
}
|
|
|
|
starlog.Infoln(h.Name + " Listening on " + h.Addr + ":" + strconv.Itoa(h.Port))
|
|
|
|
if !h.UsingSSL {
|
|
|
|
if err := h.httpserver.ListenAndServe(); err != http.ErrServerClosed {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if h.httpserver.ListenAndServeTLS(h.Cert, h.Key); err != http.ErrServerClosed {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) Close() error {
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
return h.httpserver.Shutdown(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
|
conn, err := net.DialTimeout(network, addr, time.Second*20)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
host, _, err := net.SplitHostPort(addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if h.Host != "" {
|
|
|
|
host = h.Host
|
|
|
|
}
|
|
|
|
cfg := &tls.Config{ServerName: host}
|
|
|
|
tlsConn := tls.Client(conn, cfg)
|
|
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cs := tlsConn.ConnectionState()
|
|
|
|
cert := cs.PeerCertificates[0]
|
|
|
|
|
|
|
|
// Verify here
|
|
|
|
if !h.SkipSSLVerify {
|
|
|
|
err = cert.VerifyHostname(host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tlsConn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) init() error {
|
|
|
|
h.proxy = make(map[string]*rp.ReverseProxy)
|
|
|
|
for key, val := range h.ReverseURL {
|
|
|
|
h.proxy[key] = &rp.ReverseProxy{
|
|
|
|
Transport: &http.Transport{DialTLSContext: h.dialTLS},
|
|
|
|
}
|
|
|
|
h.proxy[key].ModifyResponse = h.ModifyResponse()
|
|
|
|
h.proxy[key].Director = func(req *http.Request) {
|
|
|
|
targetQuery := val.RawQuery
|
|
|
|
req.URL.Scheme = val.Scheme
|
|
|
|
if h.Host == "" {
|
|
|
|
req.Host = val.Host
|
|
|
|
} else {
|
|
|
|
req.Host = h.Host
|
|
|
|
}
|
|
|
|
req.URL.Host = val.Host
|
|
|
|
req.URL.Path, req.URL.RawPath = joinURLPath(val, req.URL, key)
|
|
|
|
if targetQuery == "" || req.URL.RawQuery == "" {
|
|
|
|
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
|
|
|
} else {
|
|
|
|
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
|
|
|
}
|
|
|
|
h.ModifyRequest(req, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) SetResponseHeader(resp http.ResponseWriter) {
|
|
|
|
resp.Header().Set("X-Powered-By", "B612.ME")
|
|
|
|
resp.Header().Set("Server", "B612/"+version)
|
|
|
|
resp.Header().Set("X-Proxy", "B612 Reverse Proxy")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) ModifyResponse() func(*http.Response) error {
|
|
|
|
return func(resp *http.Response) error {
|
|
|
|
for _, v := range h.OutHeader {
|
|
|
|
resp.Header.Set(v[0], v[1])
|
|
|
|
}
|
|
|
|
resp.Header.Del("X-Powered-By")
|
|
|
|
resp.Header.Del("Server")
|
|
|
|
resp.Header.Del("X-Proxy")
|
|
|
|
resp.Header.Set("X-Powered-By", "B612.ME")
|
|
|
|
resp.Header.Set("Server", "B612/"+version)
|
|
|
|
resp.Header.Set("X-Proxy", "B612 Reverse Proxy")
|
|
|
|
if len(h.ReplaceList) != 0 && resp.ContentLength <= 20*1024*1024 && strings.Contains(resp.Header.Get("Content-Type"), "text") {
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
strBody := string(body)
|
|
|
|
replaceCount := -1
|
|
|
|
if h.ReplaceOnce {
|
|
|
|
replaceCount = 1
|
|
|
|
}
|
|
|
|
for _, v := range h.ReplaceList {
|
|
|
|
strBody = strings.Replace(strBody, v[0], v[1], replaceCount)
|
|
|
|
}
|
|
|
|
body = []byte(strBody)
|
|
|
|
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
|
|
|
|
resp.ContentLength = int64(len(body))
|
|
|
|
resp.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) isInCIDR(ip string) bool {
|
|
|
|
nip := net.ParseIP(strings.TrimSpace(ip))
|
|
|
|
if nip == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, c := range h.CIDR {
|
|
|
|
if c.Contains(nip) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) ModifyRequest(req *http.Request, remote *url.URL) {
|
|
|
|
switch h.IPFilterMode {
|
|
|
|
case 1:
|
|
|
|
req.Header.Set("X-Forwarded-For", strings.Split(req.RemoteAddr, ":")[0])
|
|
|
|
case 2:
|
|
|
|
xforward := strings.Split(strings.TrimSpace(req.Header.Get("X-Forwarded-For")), ",")
|
|
|
|
xforward = append(xforward, strings.Split(req.RemoteAddr, ":")[0])
|
|
|
|
req.Header.Set("X-Forwarded-For", strings.Join(xforward, ", "))
|
|
|
|
case 3:
|
|
|
|
var lastForwardIP string
|
|
|
|
var xforward []string
|
|
|
|
if h.FilterMustKey != "" && req.Header.Get(h.FilterMustKey) != "" {
|
|
|
|
lastForwardIP = req.Header.Get(h.FilterMustKey)
|
|
|
|
xforward = []string{lastForwardIP}
|
|
|
|
} else {
|
|
|
|
for _, ip := range append(strings.Split(strings.TrimSpace(req.Header.Get("X-Forwarded-For")), ","), strings.Split(req.RemoteAddr, ":")[0]) {
|
|
|
|
ip = strings.TrimSpace(ip)
|
|
|
|
if !h.isInCIDR(ip) {
|
|
|
|
xforward = append(xforward, ip)
|
|
|
|
lastForwardIP = ip
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if lastForwardIP == "" {
|
|
|
|
lastForwardIP = strings.Split(req.RemoteAddr, ":")[0]
|
|
|
|
}
|
|
|
|
if h.FilterXForward {
|
|
|
|
req.Header.Set("X-Forwarded-For", strings.Join(xforward, ", "))
|
|
|
|
}
|
|
|
|
if h.FilterRemoteAddr {
|
|
|
|
req.Header.Set("X-Real-IP", lastForwardIP)
|
|
|
|
}
|
|
|
|
if h.FilterSetKey != "" {
|
|
|
|
req.Header.Set(h.FilterSetKey, lastForwardIP)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range h.Cookie {
|
|
|
|
req.AddCookie(&http.Cookie{
|
|
|
|
Name: v[1],
|
|
|
|
Value: v[2],
|
|
|
|
Path: v[0],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
for _, v := range h.InHeader {
|
|
|
|
req.Header.Set(v[0], v[1])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) GiveBasicAuth(w http.ResponseWriter) {
|
|
|
|
w.Header().Set("WWW-Authenticate", ` Basic realm="Please Enter Passwd"`)
|
|
|
|
w.WriteHeader(401)
|
|
|
|
w.Write([]byte(`
|
|
|
|
<html>
|
|
|
|
<head><title>401 Authorization Required</title></head>
|
|
|
|
<body>
|
|
|
|
<center><h1>401 Authorization Required</h1></center>
|
|
|
|
<hr><center>B612 HTTP SERVER</center>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
if h.basicAuthPwd != "" {
|
|
|
|
if len(h.protectAuthPage) != 0 {
|
|
|
|
for _, v := range h.protectAuthPage {
|
|
|
|
if !(strings.Index(r.URL.Path, v) == 0 || strings.Contains(r.URL.RawQuery, v)) {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
|
|
if len(authHeader) == 0 {
|
|
|
|
h.GiveBasicAuth(w)
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
userAuth := base64.StdEncoding.EncodeToString([]byte(h.basicAuthUser + ":" + h.basicAuthPwd))
|
|
|
|
authStr := strings.Split(authHeader, " ")
|
|
|
|
if strings.TrimSpace(authStr[1]) != userAuth || strings.ToLower(strings.TrimSpace(authStr[0])) != "basic" {
|
|
|
|
h.GiveBasicAuth(w)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *ReverseConfig) filter(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
if len(h.blackip) == 0 && len(h.whiteip) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if len(h.whiteip) != 0 {
|
|
|
|
if _, ok := h.whiteip[strings.Split(r.RemoteAddr, ":")[0]]; ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for k, v := range h.whiteip {
|
|
|
|
if match, _ := IPinRange2(k, v, strings.Split(r.RemoteAddr, ":")[0]); match {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if _, ok := h.blackip[strings.Split(r.RemoteAddr, ":")[0]]; ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for k, v := range h.blackip {
|
|
|
|
if match, _ := IPinRange2(k, v, strings.Split(r.RemoteAddr, ":")[0]); match {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func joinURLPath(a, b *url.URL, hpath string) (path, rawpath string) {
|
|
|
|
if hpath != "/" {
|
|
|
|
b.Path = strings.TrimPrefix(b.Path, hpath)
|
|
|
|
b.RawPath = strings.TrimPrefix(b.RawPath, hpath)
|
|
|
|
}
|
|
|
|
if a.RawPath == "" && b.RawPath == "" {
|
|
|
|
return singleJoiningSlash(a.Path, b.Path), ""
|
|
|
|
}
|
|
|
|
// Same as singleJoiningSlash, but uses EscapedPath to determine
|
|
|
|
// whether a slash should be added
|
|
|
|
|
|
|
|
apath := a.EscapedPath()
|
|
|
|
bpath := b.EscapedPath()
|
|
|
|
|
|
|
|
aslash := strings.HasSuffix(apath, "/")
|
|
|
|
bslash := strings.HasPrefix(bpath, "/")
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case aslash && bslash:
|
|
|
|
return a.Path + b.Path[1:], apath + bpath[1:]
|
|
|
|
case !aslash && !bslash:
|
|
|
|
return a.Path + "/" + b.Path, apath + "/" + bpath
|
|
|
|
}
|
|
|
|
return a.Path + b.Path, apath + bpath
|
|
|
|
}
|
|
|
|
|
|
|
|
func singleJoiningSlash(a, b string) string {
|
|
|
|
aslash := strings.HasSuffix(a, "/")
|
|
|
|
bslash := strings.HasPrefix(b, "/")
|
|
|
|
switch {
|
|
|
|
case aslash && bslash:
|
|
|
|
return a + b[1:]
|
|
|
|
case !aslash && !bslash:
|
|
|
|
return a + "/" + b
|
|
|
|
}
|
|
|
|
return a + b
|
|
|
|
}
|