diff --git a/conf.go b/conf.go index 90cdd47..dab5628 100644 --- a/conf.go +++ b/conf.go @@ -5,19 +5,23 @@ import ( "io/ioutil" ) -type conf struct { - Listen []listen - Docroot string - URLRules []rule - LocalDomains []string -} +type conf []server -type listen struct { +type server struct { Host string Port int - Cert string - Key string + Docroot string + UrlRules []rule EnableProxy bool + Vhost []vhost +} + +type vhost struct { + Docroot string + Hostname string + Cert string + Key string + UrlRules []rule } type rule struct { @@ -34,7 +38,7 @@ type target struct { Path string } -func loadConfig(fn string) (*conf, error) { +func loadConfig(fn string) (conf, error) { data, err := ioutil.ReadFile(fn) if err != nil { return nil, err @@ -45,5 +49,5 @@ func loadConfig(fn string) (*conf, error) { return nil, err } - return &c, nil + return c, nil } diff --git a/config_example.yaml b/config_example.yaml index 215ac8c..5c80662 100644 --- a/config_example.yaml +++ b/config_example.yaml @@ -1,84 +1,129 @@ -# vim: set ft=yaml: -# -# server config file - -# document root -docroot: /var/www/html - -# used for http2 to determine local path request -# or proxy request -localdomains: - - www.simicloud.com - - localhost - - 127.0.0.1 - -# listener -listen: - - - # listen address - host: 0.0.0.0 - - # listen port - port: 80 - - # enable proxy or not - enableproxy: false - - - host: 0.0.0.0 - port: 443 - # server certificate - cert: server.crt - # server private key - key: server.key - enableproxy: false - -# url rules -urlrules: - - - # alias map url to path - urlprefix: /robots.txt - - # available type: alias, fastcgi, uwsgi, http - type: alias - - # target - target: - # availabe target type for alias: file, dir - type: file - path: /home/aaa/robots.txt - - - # alias map url to path - urlprefix: /cc - type: alias - target: - type: dir - path: /home/cc - - - # pass to uwsgi server - urlprefix: /media - type: uwsgi - target: - # available target type for uwsgi: unix, tcp - type: unix - path: /path/to/media.sock - - - # pass all php script to fastcgi server - urlprefix: \.php$|\.php/ - - # use regex to match the url - isregex: true - - type: fastcgi - target: - # available target type for fastcgi: unix, tcp - type: unix - path: /path/to/php.sock - - - urlprefix: /auth - # http resverse proxy - type: http - target: - # available http type for http: http, unix - type: http - host: 127.0.0.1 - port: 8080 +# vim: set ft=yaml: + +# when provide certficate file, server will listen https and enable http2 + + +# http config +- + # listen address + host: 0.0.0.0 + + # listen port + port: 9001 + + # default document root + docroot: /srv/www + + enableproxy: false + + # default host's url rule + #urlrules: + # - + # urlprefix: /a + # type: alias + # target: + # type: dir + # path: /home/user1/a + # - + # urlprefix: /b/a.txt + # type: alias + # target: + # type: file + # path: /home/user1/a/b/a.txt + + # virtual host config + vhost: + - &example1_www + hostname: www.example1.com + docroot: /var/www/html/ + # cert: + # key: + + # url rule for www.example.com + urlrules: + - + # url start with /APIv1/ forward to uwsg socket + urlprefix: /APIv1/ + type: uwsgi + target: + type: unix + path: /run/uwsgi/APIv1.sock + - + # route php script to fastCGI socket + urlprefix: /phpmyadmin/.*\.php$ + + # set to true means urlprefix is regex expression + isregex: true + + type: fastcgi + target: + type: unix + path: /var/run/php-fpm/www.sock + - + # url start with /proxy/ forward to http://10.10.1.1 + # this act as reverse proxy + urlprefix: /proxy/ + type: http + target: + type: http + host: 10.10.1.1 + port: 8080 + - &example1 + <<: *example1_www + hostname: example1.com + + - &example_www + hostname: www.example.com + docroot: /var/www/example + urlrules: + - + urlprefix: /APIv2 + type: uwsgi + target: + type: unix + path: /run/uwsgi/APIv2.sock + - &example + <<: *example_www + hostname: example.com + + - &example_bbs + hostname: bbs.example.com + docroot: /var/www/example_bbs/ + urlrules: + - + #urlprefix: \.php$|\.php\/.* + #isregex: true + + urlprefix: / + type: fastcgi + target: + type: unix + path: /var/run/php-fpm/www.sock + +# https config +- + host: 0.0.0.0 + port: 9002 + docroot: /srv/www + enableproxy: false + vhost: + - + <<: *example1 + cert: /home/user1/cert/example1.com.crt + key: /home/user1/cert/example1.com.key + - + <<: *example1_www + cert: /home/user1/cert/example1.com.crt + key: /home/user1/cert/example1.com.key + - + <<: *example_www + cert: /etc/letsencrypt/live/example.com/fullchain.pem + key: /etc/letsencrypt/live/example.com/privkey.pem + - + <<: *example + cert: /etc/letsencrypt/live/example.com/fullchain.pem + key: /etc/letsencrypt/live/example.com/privkey.pem + - + <<: *example_bbs + cert: /etc/letsencrypt/live/bbs.example.com/fullchain.pem + key: /etc/letsencrypt/live/bbs.example.com/privkey.pem diff --git a/fastcgi.go b/fastcgi.go index 24f60c2..846a5fb 100644 --- a/fastcgi.go +++ b/fastcgi.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "github.com/yookoala/gofast" "log" "net" @@ -35,6 +36,15 @@ func (f *FastCGI) ServeHTTP(w http.ResponseWriter, r *http.Request) { // FastCGIPass pass the request to fastcgi socket func (f *FastCGI) FastCGIPass(w http.ResponseWriter, r *http.Request) { + // make sure server not access the file out of document root + p1 := filepath.Clean(filepath.Join(f.DocRoot, r.URL.Path)) + p2 := filepath.Clean(f.DocRoot) + if !strings.HasPrefix(p1, p2) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "invalid url") + return + } + var scriptName, pathInfo, scriptFileName string conn, err := net.Dial(f.Network, f.Addr) @@ -58,7 +68,7 @@ func (f *FastCGI) FastCGIPass(w http.ResponseWriter, r *http.Request) { if len(p) < 2 { if strings.HasSuffix(r.URL.Path, "/") { // redirect to index.php - scriptName = "" + scriptName = filepath.Join(r.URL.Path, "index.php") pathInfo = "" scriptFileName = filepath.Join(f.DocRoot, urlPath, "index.php") } else { @@ -75,6 +85,10 @@ func (f *FastCGI) FastCGIPass(w http.ResponseWriter, r *http.Request) { req := client.NewRequest(r) + req.Params["DOCUMENT_URI"] = scriptName + req.Params["SCRIPT_NAME"] = scriptName + req.Params["PHP_SELF"] = scriptName + req.Params["DOCUMENT_ROOT"] = f.DocRoot req.Params["PATH_INFO"] = pathInfo req.Params["SCRIPT_FILENAME"] = scriptFileName diff --git a/handler.go b/handler.go index e2a0ed6..9536a5c 100644 --- a/handler.go +++ b/handler.go @@ -11,6 +11,7 @@ import ( ) type handler struct { + handler http.Handler enableProxy bool localDomains []string } @@ -25,12 +26,20 @@ var defaultTransport http.RoundTripper = &http.Transport{ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 1 && r.RequestURI[0] == '/' { - http.DefaultServeMux.ServeHTTP(w, r) + if h.handler != nil { + h.handler.ServeHTTP(w, r) + } else { + http.DefaultServeMux.ServeHTTP(w, r) + } return } if r.ProtoMajor == 2 && h.isLocalRequest(r) { - http.DefaultServeMux.ServeHTTP(w, r) + if h.handler != nil { + h.handler.ServeHTTP(w, r) + } else { + http.DefaultServeMux.ServeHTTP(w, r) + } return } diff --git a/routers.go b/routers.go index bd7490c..4533ffc 100644 --- a/routers.go +++ b/routers.go @@ -1,37 +1,108 @@ package main import ( + "crypto/tls" "fmt" "github.com/gorilla/mux" + "net" "net/http" //"net/url" + "log" "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) +func initRouters(cfg conf) { + + for _, l := range cfg { + router := mux.NewRouter() + domains := []string{} + certs := []tls.Certificate{} + + // initial virtual host + for _, h := range l.Vhost { + h2 := h.Hostname + if h1, _, err := net.SplitHostPort(h.Hostname); err == nil { + h2 = h1 + } + domains = append(domains, h2) + if h.Cert != "" && h.Key != "" { + if cert, err := tls.LoadX509KeyPair(h.Cert, h.Key); err == nil { + certs = append(certs, cert) + } else { + log.Fatal(err) + } + } + r := router.Host(h2).Subrouter() + for _, rule := range h.UrlRules { + switch rule.Type { + case "alias": + registerAliasHandler(rule, r) + case "uwsgi": + registerUwsgiHandler(rule, r) + case "fastcgi": + registerFastCGIHandler(rule, h.Docroot, r) + case "http": + registerHTTPHandler(rule, r) + default: + fmt.Printf("invalid type: %s\n", rule.Type) + } + } + r.PathPrefix("/").Handler(http.FileServer(http.Dir(h.Docroot))) + } + + // default host config + for _, rule := range l.UrlRules { + switch rule.Type { + case "alias": + registerAliasHandler(rule, router) + case "uwsgi": + registerUwsgiHandler(rule, router) + case "fastcgi": + registerFastCGIHandler(rule, l.Docroot, router) + case "http": + registerHTTPHandler(rule, router) + default: + fmt.Printf("invalid type: %s\n", rule.Type) + } } - } - router.PathPrefix("/").Handler(http.FileServer(http.Dir(cfg.Docroot))) + router.PathPrefix("/").Handler(http.FileServer(http.Dir(l.Docroot))) - http.Handle("/", router) + go func(l server) { + addr := fmt.Sprintf("%s:%d", l.Host, l.Port) + hdlr := &handler{ + handler: router, + enableProxy: l.EnableProxy, + localDomains: domains, + } + if len(certs) > 0 { + tlsconfig := &tls.Config{ + Certificates: certs, + } + + tlsconfig.BuildNameToCertificate() + + srv := http.Server{ + Addr: addr, + TLSConfig: tlsconfig, + Handler: hdlr, + } + log.Printf("listen https on %s", addr) + if err := srv.ListenAndServeTLS("", ""); err != nil { + log.Fatal(err) + } + + } else { + log.Printf("listen http on %s", addr) + if err := http.ListenAndServe(addr, hdlr); err != nil { + log.Fatal(err) + } + } + }(l) + } } func registerAliasHandler(r rule, router *mux.Router) { @@ -45,6 +116,7 @@ func registerAliasHandler(r rule, router *mux.Router) { os.Exit(-1) } } + func registerFileHandler(r rule, router *mux.Router) { router.HandleFunc(r.URLPrefix, func(w http.ResponseWriter, req *http.Request) { diff --git a/server.go b/server.go index e897132..13f4c5a 100644 --- a/server.go +++ b/server.go @@ -2,29 +2,11 @@ package main import ( "flag" - "fmt" + //"fmt" "log" - "net/http" + //"net/http" ) -func initListeners(c *conf) { - for _, l := range c.Listen { - go func(l listen) { - addr := fmt.Sprintf("%s:%d", l.Host, l.Port) - h := &handler{enableProxy: l.EnableProxy, localDomains: c.LocalDomains} - if l.Cert != "" && l.Key != "" { - if err := http.ListenAndServeTLS(addr, l.Cert, l.Key, h); err != nil { - log.Fatal(err) - } - } else { - if err := http.ListenAndServe(addr, h); err != nil { - log.Fatal(err) - } - } - }(l) - } -} - func main() { var configfile string flag.StringVar(&configfile, "c", "config.yaml", "config file") @@ -34,6 +16,5 @@ func main() { log.Fatal(err) } initRouters(c) - initListeners(c) select {} }