mirror of
https://github.com/luscis/openlan.git
synced 2025-12-24 11:10:54 +08:00
188 lines
4.5 KiB
Go
Executable File
188 lines
4.5 KiB
Go
Executable File
package proxy
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"github.com/luscis/openlan/pkg/config"
|
|
"github.com/luscis/openlan/pkg/libol"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type HttpProxy struct {
|
|
users map[string]string
|
|
out *libol.SubLogger
|
|
server *http.Server
|
|
cfg *config.HttpProxy
|
|
}
|
|
|
|
var (
|
|
connectOkay = []byte("HTTP/1.1 200 Connection established\r\n\r\n")
|
|
)
|
|
|
|
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
|
const prefix = "Basic "
|
|
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
|
return
|
|
}
|
|
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
|
if err != nil {
|
|
return
|
|
}
|
|
cs := string(c)
|
|
s := strings.IndexByte(cs, ':')
|
|
if s < 0 {
|
|
return
|
|
}
|
|
return cs[:s], cs[s+1:], true
|
|
}
|
|
|
|
func NewHttpProxy(cfg *config.HttpProxy) *HttpProxy {
|
|
h := &HttpProxy{
|
|
out: libol.NewSubLogger(cfg.Listen),
|
|
cfg: cfg,
|
|
}
|
|
h.server = &http.Server{
|
|
Addr: cfg.Listen,
|
|
Handler: h,
|
|
}
|
|
auth := cfg.Auth
|
|
if len(auth.Username) > 0 {
|
|
h.users = make(map[string]string, 1)
|
|
h.users[auth.Username] = auth.Password
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (t *HttpProxy) isAuth(username, password string) bool {
|
|
if p, ok := t.users[username]; ok {
|
|
return p == password
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *HttpProxy) CheckAuth(w http.ResponseWriter, r *http.Request) bool {
|
|
if len(t.users) == 0 {
|
|
return true
|
|
}
|
|
auth := r.Header.Get("Proxy-Authorization")
|
|
user, password, ok := parseBasicAuth(auth)
|
|
if !ok || !t.isAuth(user, password) {
|
|
w.Header().Set("Proxy-Authenticate", "Basic")
|
|
http.Error(w, "Proxy Authentication Required", http.StatusProxyAuthRequired)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (t *HttpProxy) route(w http.ResponseWriter, p *http.Response) {
|
|
defer p.Body.Close()
|
|
for key, value := range p.Header {
|
|
if key == "Proxy-Authorization" {
|
|
if len(value) > 0 { // Remove first value for next proxy.
|
|
value = value[1:]
|
|
}
|
|
}
|
|
for _, v := range value {
|
|
w.Header().Add(key, v)
|
|
}
|
|
}
|
|
w.WriteHeader(p.StatusCode)
|
|
_, _ = io.Copy(w, p.Body)
|
|
}
|
|
|
|
func (t *HttpProxy) tunnel(w http.ResponseWriter, conn net.Conn) {
|
|
src, bio, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer src.Close()
|
|
wait := libol.NewWaitOne(2)
|
|
libol.Go(func() {
|
|
defer wait.Done()
|
|
// The returned bufio.Reader may contain unprocessed buffered data from the client.
|
|
// Copy them to dst so we can use src directly.
|
|
if n := bio.Reader.Buffered(); n > 0 {
|
|
n64, err := io.CopyN(conn, bio, int64(n))
|
|
if n64 != int64(n) || err != nil {
|
|
t.out.Warn("HttpProxy.tunnel io.CopyN:", n64, err)
|
|
return
|
|
}
|
|
}
|
|
if _, err := io.Copy(conn, src); err != nil {
|
|
t.out.Debug("HttpProxy.tunnel from ws %s", err)
|
|
}
|
|
})
|
|
libol.Go(func() {
|
|
defer wait.Done()
|
|
if _, err := io.Copy(src, conn); err != nil {
|
|
t.out.Debug("HttpProxy.tunnel from target %s", err)
|
|
}
|
|
})
|
|
wait.Wait()
|
|
t.out.Debug("HttpProxy.tunnel %s exit", conn.RemoteAddr())
|
|
}
|
|
|
|
func (t *HttpProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
t.out.Debug("HttpProxy.ServeHTTP %v", r)
|
|
t.out.Debug("HttpProxy.ServeHTTP %v", r.URL.Host)
|
|
if !t.CheckAuth(w, r) {
|
|
t.out.Info("HttpProxy.ServeHTTP Required %v Authentication", r.URL.Host)
|
|
return
|
|
}
|
|
t.out.Info("HttpProxy.ServeHTTP %s %s -> %s", r.Method, r.RemoteAddr, r.URL.Host)
|
|
if r.Method == "CONNECT" { //RFC-7231 Tunneling TCP based protocols through Web Proxy servers
|
|
conn, err := net.Dial("tcp", r.URL.Host)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
_, _ = w.Write(connectOkay)
|
|
t.tunnel(w, conn)
|
|
} else { //RFC 7230 - HTTP/1.1: Message Syntax and Routing
|
|
transport := &http.Transport{}
|
|
p, err := transport.RoundTrip(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer transport.CloseIdleConnections()
|
|
t.route(w, p)
|
|
}
|
|
}
|
|
|
|
func (t *HttpProxy) Start() {
|
|
if t.server == nil || t.cfg == nil {
|
|
return
|
|
}
|
|
crt := t.cfg.Cert
|
|
if crt == nil || crt.KeyFile == "" {
|
|
t.out.Info("HttpProxy.start http://%s", t.server.Addr)
|
|
} else {
|
|
t.out.Info("HttpProxy.start https://%s", t.server.Addr)
|
|
}
|
|
promise := &libol.Promise{
|
|
First: time.Second * 2,
|
|
MaxInt: time.Minute,
|
|
MinInt: time.Second * 10,
|
|
}
|
|
promise.Go(func() error {
|
|
if crt == nil || crt.KeyFile == "" {
|
|
if err := t.server.ListenAndServe(); err != nil {
|
|
t.out.Warn("HttpProxy.start %s", err)
|
|
return err
|
|
}
|
|
} else {
|
|
if err := t.server.ListenAndServeTLS(crt.CrtFile, crt.KeyFile); err != nil {
|
|
t.out.Error("HttpProxy.start %s", err)
|
|
return err
|
|
}
|
|
}
|
|
t.server.Shutdown(nil)
|
|
return nil
|
|
})
|
|
}
|