diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..b668dbc --- /dev/null +++ b/conf.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/go-yaml/yaml" + "io/ioutil" +) + +type conf struct { + Listen []listen + Docroot string + URLRules []rule +} + +type listen struct { + Host string + Port string + Cert string + Key string + EnableProxy bool +} + +type rule struct { + URLPrefix string + IsRegex bool + Type string + Target target +} + +type target struct { + Type string + Host string + Port int + Path string +} + +func loadConfig(fn string) (*conf, error) { + data, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + + var c conf + if err := yaml.Unmarshal(data, &c); err != nil { + return nil, err + } + + return &c, nil +} diff --git a/conf_test.go b/conf_test.go new file mode 100644 index 0000000..109d7af --- /dev/null +++ b/conf_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestConf(t *testing.T) { + c, err := loadConfig("config_example.yaml") + if err != nil { + t.Fatal(err) + } + fmt.Printf("%#v\n", c) +} diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..af8eb8f --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,59 @@ + +# document root +docroot: /var/www/html + +# listener +listen: + - + host: 0.0.0.0 + port: 80 + # enable proxy + enableproxy: true + - + host: 0.0.0.0 + port: 443 + # server certificate + cert: server.crt + # server private key + key: server.key + enableproxy: false + +# url rules +urlrules: + - + urlprefix: /robots.txt + # available type: alias, fastcgi, uwsgi, http + type: alias + target: + # availabe type for alias: file, dir + type: file + path: /home/aaa/robots.txt + - + urlprefix: /cc + type: alias + target: + type: dir + path: /home/cc + - + urlprefix: /media + type: uwsgi + target: + # available type for uwsgi: unix, tcp + type: unix + path: /path/to/media.sock + - + urlprefix: \.php$|\.php/ + isregex: true + type: fastcgi + target: + # available type for fastcgi: unix, tcp + type: unix + path: /path/to/php.sock + - + urlprefix: /o + type: http + target: + # available type for http: http, unix + type: http + host: 127.0.0.1 + port: 8080 diff --git a/fastcgi.go b/fastcgi.go new file mode 100644 index 0000000..24f60c2 --- /dev/null +++ b/fastcgi.go @@ -0,0 +1,129 @@ +package main + +import ( + "github.com/yookoala/gofast" + "log" + "net" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" +) + +// FastCGI is a fastcgi client connection +type FastCGI struct { + Network string + Addr string + DocRoot string + URLPrefix string + //client gofast.Client +} + +// NewFastCGI creates a new FastCGI struct +func NewFastCGI(network, addr, docroot, urlPrefix string) (*FastCGI, error) { + u := strings.TrimRight(urlPrefix, "/") + return &FastCGI{network, addr, docroot, u}, nil +} + +var fcgiPathInfo = regexp.MustCompile(`^(.*?\.php)(.*)$`) + +// ServeHTTP implements http.Handler interface +func (f *FastCGI) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.FastCGIPass(w, r) +} + +// FastCGIPass pass the request to fastcgi socket +func (f *FastCGI) FastCGIPass(w http.ResponseWriter, r *http.Request) { + var scriptName, pathInfo, scriptFileName string + + conn, err := net.Dial(f.Network, f.Addr) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadGateway) + return + } + + defer conn.Close() + + client := gofast.NewClient(conn, 20) + + urlPath := r.URL.Path + if f.URLPrefix != "" { + urlPath = strings.Replace(r.URL.Path, f.URLPrefix, "", 1) + } + + p := fcgiPathInfo.FindStringSubmatch(urlPath) + + if len(p) < 2 { + if strings.HasSuffix(r.URL.Path, "/") { + // redirect to index.php + scriptName = "" + pathInfo = "" + scriptFileName = filepath.Join(f.DocRoot, urlPath, "index.php") + } else { + // serve static file in php directory + fn := filepath.Join(f.DocRoot, urlPath) + http.ServeFile(w, r, fn) + return + } + } else { + scriptName = p[1] + pathInfo = p[2] + scriptFileName = filepath.Join(f.DocRoot, scriptName) + } + + req := client.NewRequest(r) + + req.Params["PATH_INFO"] = pathInfo + req.Params["SCRIPT_FILENAME"] = scriptFileName + + https := "off" + scheme := "http" + if r.TLS != nil { + https = "on" + scheme = "https" + } + + req.Params["REQUEST_SCHEME"] = scheme + req.Params["HTTPS"] = https + + host, port, _ := net.SplitHostPort(r.RemoteAddr) + req.Params["REMOTE_ADDR"] = host + req.Params["REMOTE_PORT"] = port + + host, port, err = net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + if scheme == "http" { + port = "80" + } else { + port = "443" + } + } + req.Params["SERVER_NAME"] = host + req.Params["SERVER_PORT"] = port + + req.Params["SERVER_PROTOCOL"] = r.Proto + + for k, v := range r.Header { + k = "HTTP_" + strings.ToUpper(strings.Replace(k, "-", "_", -1)) + if _, ok := req.Params[k]; ok == false { + req.Params[k] = strings.Join(v, ";") + } + } + + resp, err := client.Do(req) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadGateway) + return + } + + err = resp.WriteTo(w, os.Stderr) + if err != nil { + log.Println(err) + } + + resp.Close() +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..5589129 --- /dev/null +++ b/handler.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "strings" + "time" +) + +type handler struct { + enableProxy bool +} + +var defaultTransport http.RoundTripper = &http.Transport{ + DialContext: dialContext, + MaxIdleConns: 50, + IdleConnTimeout: 30 * time.Second, + MaxIdleConnsPerHost: 3, + //ResponseHeaderTimeout: 2 * time.Second, +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.RequestURI[0] == '/' { + http.DefaultServeMux.ServeHTTP(w, r) + return + } + + if !h.enableProxy { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "

page not found!

") + return + } + if r.Method == http.MethodConnect { + h.handleCONNECT(w, r) + } else { + h.handleHTTP(w, r) + } +} + +func (h *handler) handleHTTP(w http.ResponseWriter, r *http.Request) { + + var resp *http.Response + var err error + + r.Header.Del("proxy-connection") + + resp, err = defaultTransport.RoundTrip(r) + if err != nil { + log.Printf("RoundTrip: %s", err) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(w, "%s", err) + return + } + + defer resp.Body.Close() + + hdr := w.Header() + + //resp.Header.Del("connection") + + for k, v := range resp.Header { + for _, v1 := range v { + hdr.Add(k, v1) + } + } + + w.WriteHeader(resp.StatusCode) + + io.Copy(w, resp.Body) +} + +func (h *handler) handleCONNECT(w http.ResponseWriter, r *http.Request) { + host := r.RequestURI + if !strings.Contains(host, ":") { + host = fmt.Sprintf("%s:443", host) + } + + var conn net.Conn + var err error + + conn, err = dial("tcp", host) + if err != nil { + log.Printf("net.dial: %s", err) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(w, "dial to %s failed: %s", host, err) + return + } + + hj, _ := w.(http.Hijacker) + conn1, _, _ := hj.Hijack() + + fmt.Fprintf(conn1, "%s 200 connection established\r\n\r\n", r.Proto) + + pipeAndClose(conn, conn1) +} + +func pipeAndClose(r1, r2 io.ReadWriteCloser) { + ch := make(chan int, 2) + go func() { + io.Copy(r1, r2) + ch <- 1 + }() + + go func() { + io.Copy(r2, r1) + ch <- 1 + }() + + <-ch +} diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..2ae99a2 --- /dev/null +++ b/proxy.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "net" + "net/http" + //"bufio" + "io" + "log" + "strings" + "time" + //"strings" +) + +type proxy struct { + transport http.RoundTripper + addr string + prefix string + dialer *net.Dialer +} + +func newProxy(addr string, prefix string) *proxy { + p := &proxy{ + addr: addr, + prefix: prefix, + dialer: &net.Dialer{Timeout: 2 * time.Second}, + } + p.transport = &http.Transport{ + DialContext: p.dialContext, + MaxIdleConns: 5, + IdleConnTimeout: 30 * time.Second, + //Dial: p.dial, + } + return p +} + +func (p *proxy) dialContext(ctx context.Context, network, addr string) (net.Conn, error) { + return p.dialer.DialContext(ctx, network, p.addr) +} + +func (p *proxy) dial(network, addr string) (conn net.Conn, err error) { + return p.dialer.Dial(network, p.addr) +} + +func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + host, _, _ := net.SplitHostPort(r.RemoteAddr) + r.Header.Add("X-Forwarded-For", host) + r.RequestURI = strings.TrimLeft(r.RequestURI, p.prefix) + //r.URL.Scheme = "http" + //r.URL.Host = r.Host + r.URL.Path = strings.TrimLeft(r.URL.Path, p.prefix) + resp, err := p.transport.RoundTrip(r) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadGateway) + w.Write([]byte("

502 Bad Gateway

")) + return + } + header := w.Header() + for k, v := range resp.Header { + for _, v1 := range v { + header.Add(k, v1) + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + resp.Body.Close() +} diff --git a/routers.go b/routers.go new file mode 100644 index 0000000..2dfa967 --- /dev/null +++ b/routers.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "github.com/gorilla/mux" + "net/http" + //"net/url" + "os" + "regexp" + //"path/filepath" + //"strings" +) + +func initRouters(cfg *conf) { + router := mux.NewRouter() + + for _, r := range cfg.URLRules { + switch r.Type { + case "alias": + registerAliasHandler(r, router) + case "uwsgi": + registerUwsgiHandler(r, router) + case "fastcgi": + registerFastCGIHandler(r, cfg.Docroot, router) + case "http": + registerHTTPHandler(r, router) + default: + fmt.Printf("invalid type: %s\n", r.Type) + } + } + + router.PathPrefix("/").Handler(http.FileServer(http.Dir(cfg.Docroot))) + + http.Handle("/", router) +} + +func registerAliasHandler(r rule, router *mux.Router) { + switch r.Target.Type { + case "file": + registerFileHandler(r, router) + case "dir": + registerDirHandler(r, router) + default: + fmt.Printf("invalid type: %s, only file, dir allowed\n", r.Target.Type) + os.Exit(-1) + } +} +func registerFileHandler(r rule, router *mux.Router) { + router.HandleFunc(r.URLPrefix, func(w http.ResponseWriter, req *http.Request) { + http.ServeFile(w, req, r.Target.Path) + }) +} + +func registerDirHandler(r rule, router *mux.Router) { + router.PathPrefix(r.URLPrefix).Handler(http.FileServer(http.Dir(r.Target.Path))) +} + +func registerUwsgiHandler(r rule, router *mux.Router) { + var p string + switch r.Target.Type { + case "unix": + p = r.Target.Path + case "tcp": + p = fmt.Sprintf("%s:%d", r.Target.Host, r.Target.Port) + default: + fmt.Printf("invalid scheme: %s, only support unix, tcp", r.Target.Type) + os.Exit(-1) + } + + if r.IsRegex { + m1 := myURLMatch{regexp.MustCompile(r.URLPrefix)} + u := NewUwsgi(r.Target.Type, p, "") + router.MatcherFunc(m1.match).Handler(u) + } else { + u := NewUwsgi(r.Target.Type, p, r.URLPrefix) + router.PathPrefix(r.URLPrefix).Handler(u) + } +} + +func registerFastCGIHandler(r rule, docroot string, router *mux.Router) { + var p string + switch r.Target.Type { + case "unix": + p = r.Target.Path + case "tcp": + p = fmt.Sprintf("%s:%d", r.Target.Host, r.Target.Port) + default: + fmt.Printf("invalid scheme: %s, only support unix, tcp", r.Target.Type) + os.Exit(-1) + } + if r.IsRegex { + m1 := myURLMatch{regexp.MustCompile(r.URLPrefix)} + u, _ := NewFastCGI(r.Target.Type, p, docroot, "") + router.MatcherFunc(m1.match).Handler(u) + } else { + u, _ := NewFastCGI(r.Target.Type, p, docroot, r.URLPrefix) + router.PathPrefix(r.URLPrefix).Handler(u) + } +} + +func registerHTTPHandler(r rule, router *mux.Router) { + var u http.Handler + switch r.Target.Type { + case "unix": + u = newProxy(r.Target.Path, r.URLPrefix) + case "http": + u = newProxy(fmt.Sprintf("%s:%d", r.Target.Host, r.Target.Port), r.URLPrefix) + default: + fmt.Printf("invalid scheme: %s, only support unix, http", r.Target.Type) + os.Exit(-1) + } + router.PathPrefix(r.URLPrefix).Handler(u) +} + +type myURLMatch struct { + re *regexp.Regexp +} + +func (m myURLMatch) match(r *http.Request, route *mux.RouteMatch) bool { + ret := m.re.MatchString(r.URL.Path) + return ret +} diff --git a/server.go b/server.go index 5582ad3..2c6f5aa 100644 --- a/server.go +++ b/server.go @@ -3,145 +3,38 @@ package main import ( "flag" "fmt" - "io" "log" - "net" "net/http" - "os" - "strings" - "time" ) -var defaultTransport http.RoundTripper = &http.Transport{ - DialContext: dialContext, - MaxIdleConns: 50, - IdleConnTimeout: 30 * time.Second, - MaxIdleConnsPerHost: 3, - //ResponseHeaderTimeout: 2 * time.Second, +func initListeners(c *conf) { + for _, l := range c.Listen { + go func(l listen) { + addr := fmt.Sprintf("%s:%d", l.Host, l.Port) + if l.Cert != "" && l.Key != "" { + if err := http.ListenAndServeTLS(addr, l.Cert, l.Key, + &handler{enableProxy: l.EnableProxy}); err != nil { + log.Fatal(err) + } + } else { + if err := http.ListenAndServe(addr, + &handler{enableProxy: l.EnableProxy}); err != nil { + log.Fatal(err) + } + } + }(l) + } } func main() { - var docroot string - var enableProxy bool - var port int - - curdir, err := os.Getwd() - if err != nil { - curdir = "." - } - - flag.StringVar(&docroot, "docroot", curdir, "document root") - flag.BoolVar(&enableProxy, "enable_proxy", false, "enable proxy function") - flag.IntVar(&port, "port", 8080, "the port listen to") + var configfile string + flag.StringVar(&configfile, "-c", "config.yaml", "config file") flag.Parse() - - http.Handle("/", http.FileServer(http.Dir(docroot))) - - log.Printf("Listen on :%d", port) - log.Printf("document root %s", docroot) - if enableProxy { - log.Println("proxy enabled") - } - err = http.ListenAndServe(fmt.Sprintf(":%d", port), &handler{ - enableProxy: enableProxy, - }) + c, err := loadConfig(configfile) if err != nil { log.Fatal(err) } -} - -type handler struct { - enableProxy bool -} - -func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.RequestURI[0] == '/' { - http.DefaultServeMux.ServeHTTP(w, r) - return - } - - if !h.enableProxy { - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "

page not found!

") - return - } - if r.Method == http.MethodConnect { - h.handleCONNECT(w, r) - } else { - h.handleHTTP(w, r) - } -} - -func (h *handler) handleHTTP(w http.ResponseWriter, r *http.Request) { - - var resp *http.Response - var err error - - r.Header.Del("proxy-connection") - - resp, err = defaultTransport.RoundTrip(r) - if err != nil { - log.Printf("RoundTrip: %s", err) - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "%s", err) - return - } - - defer resp.Body.Close() - - hdr := w.Header() - - //resp.Header.Del("connection") - - for k, v := range resp.Header { - for _, v1 := range v { - hdr.Add(k, v1) - } - } - - w.WriteHeader(resp.StatusCode) - - io.Copy(w, resp.Body) -} - -func (h *handler) handleCONNECT(w http.ResponseWriter, r *http.Request) { - host := r.RequestURI - if !strings.Contains(host, ":") { - host = fmt.Sprintf("%s:443", host) - } - - var conn net.Conn - var err error - - conn, err = dial("tcp", host) - if err != nil { - log.Printf("net.dial: %s", err) - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "dial to %s failed: %s", host, err) - return - } - - hj, _ := w.(http.Hijacker) - conn1, _, _ := hj.Hijack() - - fmt.Fprintf(conn1, "%s 200 connection established\r\n\r\n", r.Proto) - - pipeAndClose(conn, conn1) -} - -func pipeAndClose(r1, r2 io.ReadWriteCloser) { - ch := make(chan int, 2) - go func() { - io.Copy(r1, r2) - ch <- 1 - }() - - go func() { - io.Copy(r2, r1) - ch <- 1 - }() - - <-ch + initRouters(c) + initListeners(c) + select {} } diff --git a/uwsgi.go b/uwsgi.go new file mode 100644 index 0000000..83c7d2c --- /dev/null +++ b/uwsgi.go @@ -0,0 +1,101 @@ +package main + +import ( + //"fmt" + uwsgi "github.com/fangdingjun/go-uwsgi" + "net" + "net/http" + "strconv" + "strings" +) + +// Uwsgi is a struct for uwsgi +type Uwsgi struct { + Passenger *uwsgi.Passenger + URLPrefix string +} + +// NewUwsgi create a new Uwsgi +func NewUwsgi(network, addr, urlPrefix string) *Uwsgi { + u := strings.TrimRight(urlPrefix, "/") + return &Uwsgi{&uwsgi.Passenger{network, addr}, u} +} + +// ServeHTTP implements http.Handler interface +func (u *Uwsgi) ServeHTTP(w http.ResponseWriter, r *http.Request) { + u.UwsgiPass(w, r) +} + +// UwsgiPass pass the request to uwsgi interface +func (u *Uwsgi) UwsgiPass(w http.ResponseWriter, r *http.Request) { + params := buildParams(r, u.URLPrefix) + u.Passenger.UwsgiPass(w, r, params) +} + +func buildParams(req *http.Request, urlPrefix string) map[string][]string { + var err error + + header := make(map[string][]string) + + if urlPrefix != "" { + header["SCRIPT_NAME"] = []string{urlPrefix} + p := strings.Replace(req.URL.Path, urlPrefix, "", 1) + header["PATH_INFO"] = []string{p} + } else { + header["PATH_INFO"] = []string{req.URL.Path} + } + + //fmt.Printf("url: %s, scheme: %s\n", req.URL.String(), req.URL.Scheme) + + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + header["REQUEST_SCHEME"] = []string{scheme} + + header["HTTPS"] = []string{"off"} + + /* https */ + if scheme == "https" { + header["HTTPS"] = []string{"on"} + } + + /* speicial port */ + host, port, err := net.SplitHostPort(req.Host) + if err != nil { + host = req.Host + if scheme == "http" { + port = "80" + } else { + port = "443" + } + } + header["SERVER_NAME"] = []string{host} + header["SERVER_PORT"] = []string{port} + + host, port, err = net.SplitHostPort(req.RemoteAddr) + if err != nil { + host = req.RemoteAddr + port = "80" + } + header["REMOTE_PORT"] = []string{port} + header["REMOTE_ADDR"] = []string{host} + + header["REQUEST_METHOD"] = []string{req.Method} + header["REQUEST_URI"] = []string{req.RequestURI} + header["CONTENT_LENGTH"] = []string{strconv.Itoa(int(req.ContentLength))} + header["SERVER_PROTOCOL"] = []string{req.Proto} + header["QUERY_STRING"] = []string{req.URL.RawQuery} + + if ctype := req.Header.Get("Content-Type"); ctype != "" { + header["CONTENT_TYPE"] = []string{ctype} + } + + for k, v := range req.Header { + k = "HTTP_" + strings.ToUpper(strings.Replace(k, "-", "_", -1)) + if _, ok := header[k]; ok == false { + header[k] = v + } + } + return header +}