add fastcgi, uwsgi, proxy_pass support

master
fangdingjun 8 years ago
parent 15a04fa023
commit 1617034fad

@ -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
}

@ -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)
}

@ -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

@ -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()
}

@ -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, "<h1>page not found!</h1>")
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
}

@ -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("<h1>502 Bad Gateway</h1>"))
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()
}

@ -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
}

@ -3,145 +3,38 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"net"
"net/http" "net/http"
"os"
"strings"
"time"
) )
var defaultTransport http.RoundTripper = &http.Transport{ func initListeners(c *conf) {
DialContext: dialContext, for _, l := range c.Listen {
MaxIdleConns: 50, go func(l listen) {
IdleConnTimeout: 30 * time.Second, addr := fmt.Sprintf("%s:%d", l.Host, l.Port)
MaxIdleConnsPerHost: 3, if l.Cert != "" && l.Key != "" {
//ResponseHeaderTimeout: 2 * time.Second, 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() { func main() {
var docroot string var configfile string
var enableProxy bool flag.StringVar(&configfile, "-c", "config.yaml", "config file")
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")
flag.Parse() flag.Parse()
c, err := loadConfig(configfile)
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,
})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} initRouters(c)
initListeners(c)
type handler struct { select {}
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, "<h1>page not found!</h1>")
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
} }

@ -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
}
Loading…
Cancel
Save