You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
star/httpreverse/service.go

316 lines
8.2 KiB
Go

package httpreverse
import (
"b612.me/starlog"
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"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]*httputil.ReverseProxy)
for key, val := range h.ReverseURL {
h.proxy[key] = &httputil.ReverseProxy{
Transport: &http.Transport{DialTLSContext: h.dialTLS},
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)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
},
}
h.proxy[key].ModifyResponse = h.ModifyResponse()
originalDirector := h.proxy[key].Director
h.proxy[key].Director = func(req *http.Request) {
originalDirector(req)
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) ModifyRequest(req *http.Request, remote *url.URL) {
if h.XForwardMode == 1 {
req.Header.Set("X-Forwarded-For", strings.Split(req.RemoteAddr, ":")[0])
} else if h.XForwardMode == 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, ", "))
}
for _, v := range h.Cookie {
req.AddCookie(&http.Cookie{
Name: v[1],
Value: v[2],
Path: v[0],
})
}
host := h.Host
if host == "" {
host = remote.Host
}
targetQuery := remote.RawQuery
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.Host = host
req.URL.Path, req.URL.RawPath = joinURLPath(remote, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
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) (path, rawpath string) {
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
}