mirror of
				https://github.com/ossrs/srs.git
				synced 2025-03-09 15:49:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			643 lines
		
	
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			643 lines
		
	
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| The MIT License (MIT)
 | |
| 
 | |
| Copyright (c) 2019 winlin
 | |
| 
 | |
| Permission is hereby granted, free of charge, to any person obtaining a copy
 | |
| of this software and associated documentation files (the "Software"), to deal
 | |
| in the Software without restriction, including without limitation the rights
 | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | |
| copies of the Software, and to permit persons to whom the Software is
 | |
| furnished to do so, subject to the following conditions:
 | |
| 
 | |
| The above copyright notice and this permission notice shall be included in all
 | |
| copies or substantial portions of the Software.
 | |
| 
 | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | |
| SOFTWARE.
 | |
| */
 | |
| 
 | |
| /*
 | |
|  This the main entrance of https-proxy, proxy to api or other http server.
 | |
| */
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/tls"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	oe "github.com/ossrs/go-oryx-lib/errors"
 | |
| 	oh "github.com/ossrs/go-oryx-lib/http"
 | |
| 	"github.com/ossrs/go-oryx-lib/https"
 | |
| 	ol "github.com/ossrs/go-oryx-lib/logger"
 | |
| 	"io/ioutil"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/http/httputil"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| )
 | |
| 
 | |
| type Strings []string
 | |
| 
 | |
| func (v *Strings) String() string {
 | |
| 	return fmt.Sprintf("strings [%v]", strings.Join(*v, ","))
 | |
| }
 | |
| 
 | |
| func (v *Strings) Set(value string) error {
 | |
| 	*v = append(*v, value)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func shouldProxyURL(srcPath, proxyPath string) bool {
 | |
| 	if !strings.HasSuffix(srcPath, "/") {
 | |
| 		// /api to /api/
 | |
| 		// /api.js to /api.js/
 | |
| 		// /api/100 to /api/100/
 | |
| 		srcPath += "/"
 | |
| 	}
 | |
| 
 | |
| 	if !strings.HasSuffix(proxyPath, "/") {
 | |
| 		// /api/ to /api/
 | |
| 		// to match /api/ or /api/100
 | |
| 		// and not match /api.js/
 | |
| 		proxyPath += "/"
 | |
| 	}
 | |
| 
 | |
| 	return strings.HasPrefix(srcPath, proxyPath)
 | |
| }
 | |
| 
 | |
| // about x-real-ip and x-forwarded-for or
 | |
| // about X-Real-IP and X-Forwarded-For or
 | |
| // https://segmentfault.com/q/1010000002409659
 | |
| // https://distinctplace.com/2014/04/23/story-behind-x-forwarded-for-and-x-real-ip-headers/
 | |
| // @remark http proxy will set the X-Forwarded-For.
 | |
| func addProxyAddToHeader(remoteAddr, realIP string, fwd []string, header http.Header, omitForward bool) {
 | |
| 	rip, _, err := net.SplitHostPort(remoteAddr)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if realIP != "" {
 | |
| 		header.Set("X-Real-IP", realIP)
 | |
| 	} else {
 | |
| 		header.Set("X-Real-IP", rip)
 | |
| 	}
 | |
| 
 | |
| 	if !omitForward {
 | |
| 		header["X-Forwarded-For"] = fwd[:]
 | |
| 		header.Add("X-Forwarded-For", rip)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func filterByPreHook(ctx context.Context, preHook *url.URL, req *http.Request) error {
 | |
| 	target := *preHook
 | |
| 	target.RawQuery = strings.Join([]string{target.RawQuery, req.URL.RawQuery}, "&")
 | |
| 
 | |
| 	api := target.String()
 | |
| 	r, err := http.NewRequestWithContext(ctx, req.Method, api, nil)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Add real ip and forwarded for to header.
 | |
| 	// We should append the forward for pass-by.
 | |
| 	addProxyAddToHeader(req.RemoteAddr, req.Header.Get("X-Real-IP"), req.Header["X-Forwarded-For"], r.Header, false)
 | |
| 	ol.Tf(ctx, "Pre-hook proxy addr req=%v, r=%v", req.Header, r.Header)
 | |
| 
 | |
| 	r2, err := http.DefaultClient.Do(r)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer r2.Body.Close()
 | |
| 
 | |
| 	if r2.StatusCode != http.StatusOK {
 | |
| 		return fmt.Errorf("Pre-hook HTTP StatusCode=%v %v", r2.StatusCode, r2.Status)
 | |
| 	}
 | |
| 
 | |
| 	b, err := ioutil.ReadAll(r2.Body)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	ol.Tf(ctx, "Pre-hook %v url=%v, res=%v, headers=%v", req.Method, api, string(b), r.Header)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func NewComplexProxy(ctx context.Context, proxyUrl, preHook *url.URL, originalRequest *http.Request) http.Handler {
 | |
| 	// Hook before proxy it.
 | |
| 	if preHook != nil {
 | |
| 		if err := filterByPreHook(ctx, preHook, originalRequest); err != nil {
 | |
| 			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 				ol.Ef(ctx, "Pre-hook err %+v", err)
 | |
| 				http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Start proxy it.
 | |
| 	proxy := &httputil.ReverseProxy{}
 | |
| 	proxyUrlQuery := proxyUrl.Query()
 | |
| 
 | |
| 	// Create a proxy which attach a isolate logger.
 | |
| 	elogger := log.New(os.Stderr, fmt.Sprintf("%v ", originalRequest.RemoteAddr), log.LstdFlags)
 | |
| 	proxy.ErrorLog = elogger
 | |
| 
 | |
| 	proxy.Director = func(r *http.Request) {
 | |
| 		// about the x-real-schema, we proxy to backend to identify the client schema.
 | |
| 		if rschema := r.Header.Get("X-Real-Schema"); rschema == "" {
 | |
| 			if r.TLS == nil {
 | |
| 				r.Header.Set("X-Real-Schema", "http")
 | |
| 			} else {
 | |
| 				r.Header.Set("X-Real-Schema", "https")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Add real ip and forwarded for to header.
 | |
| 		// We should omit the forward header, because the ReverseProxy will doit.
 | |
| 		addProxyAddToHeader(r.RemoteAddr, r.Header.Get("X-Real-IP"), r.Header["X-Forwarded-For"], r.Header, true)
 | |
| 		ol.Tf(ctx, "Proxy addr header %v", r.Header)
 | |
| 
 | |
| 		r.URL.Scheme = proxyUrl.Scheme
 | |
| 		r.URL.Host = proxyUrl.Host
 | |
| 
 | |
| 		// Trim the prefix path.
 | |
| 		if trimPrefix := proxyUrlQuery.Get("trimPrefix"); trimPrefix != "" {
 | |
| 			r.URL.Path = strings.TrimPrefix(r.URL.Path, trimPrefix)
 | |
| 		}
 | |
| 		// Aadd the prefix to path.
 | |
| 		if addPrefix := proxyUrlQuery.Get("addPrefix"); addPrefix != "" {
 | |
| 			r.URL.Path = addPrefix + r.URL.Path
 | |
| 		}
 | |
| 
 | |
| 		// The original request.Host requested by the client.
 | |
| 		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
 | |
| 		if r.Header.Get("X-Forwarded-Host") == "" {
 | |
| 			r.Header.Set("X-Forwarded-Host", r.Host)
 | |
| 		}
 | |
| 
 | |
| 		// Set the Host of client request to the upstream server's, to act as client
 | |
| 		// directly access the upstream server.
 | |
| 		if proxyUrlQuery.Get("modifyRequestHost") != "false" {
 | |
| 			r.Host = proxyUrl.Host
 | |
| 		}
 | |
| 
 | |
| 		ra, url, rip := r.RemoteAddr, r.URL.String(), r.Header.Get("X-Real-Ip")
 | |
| 		ol.Tf(ctx, "proxy http rip=%v, addr=%v %v %v with headers %v", rip, ra, r.Method, url, r.Header)
 | |
| 	}
 | |
| 
 | |
| 	proxy.ModifyResponse = func(w *http.Response) error {
 | |
| 		// We have already set the server, so remove the upstream one.
 | |
| 		if proxyUrlQuery.Get("keepUpsreamServer") != "true" {
 | |
| 			w.Header.Del("Server")
 | |
| 		}
 | |
| 
 | |
| 		// We already added this header, it will cause chrome failed when duplicated.
 | |
| 		if w.Header.Get("Access-Control-Allow-Origin") != "" {
 | |
| 			w.Header.Del("Access-Control-Allow-Origin")
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return proxy
 | |
| }
 | |
| 
 | |
| func run(ctx context.Context) error {
 | |
| 	oh.Server = fmt.Sprintf("%v/%v", Signature(), Version())
 | |
| 	fmt.Println(oh.Server, "HTTP/HTTPS static server with API proxy.")
 | |
| 
 | |
| 	var httpPorts Strings
 | |
| 	flag.Var(&httpPorts, "t", "http listen")
 | |
| 	flag.Var(&httpPorts, "http", "http listen at. 0 to disable http.")
 | |
| 
 | |
| 	var httpsPorts Strings
 | |
| 	flag.Var(&httpsPorts, "s", "https listen")
 | |
| 	flag.Var(&httpsPorts, "https", "https listen at. 0 to disable https. 443 to serve. ")
 | |
| 
 | |
| 	var httpsDomains string
 | |
| 	flag.StringVar(&httpsDomains, "d", "", "https the allow domains")
 | |
| 	flag.StringVar(&httpsDomains, "domains", "", "https the allow domains, empty to allow all. for example: ossrs.net,www.ossrs.net")
 | |
| 
 | |
| 	var html string
 | |
| 	flag.StringVar(&html, "r", "./html", "the www web root")
 | |
| 	flag.StringVar(&html, "root", "./html", "the www web root. support relative dir to argv[0].")
 | |
| 
 | |
| 	var cacheFile string
 | |
| 	flag.StringVar(&cacheFile, "e", "./letsencrypt.cache", "https the cache for letsencrypt")
 | |
| 	flag.StringVar(&cacheFile, "cache", "./letsencrypt.cache", "https the cache for letsencrypt. support relative dir to argv[0].")
 | |
| 
 | |
| 	var useLetsEncrypt bool
 | |
| 	flag.BoolVar(&useLetsEncrypt, "l", false, "whether use letsencrypt CA")
 | |
| 	flag.BoolVar(&useLetsEncrypt, "lets", false, "whether use letsencrypt CA. self sign if not.")
 | |
| 
 | |
| 	var ssKey string
 | |
| 	flag.StringVar(&ssKey, "k", "", "https self-sign key")
 | |
| 	flag.StringVar(&ssKey, "ssk", "", "https self-sign key")
 | |
| 
 | |
| 	var ssCert string
 | |
| 	flag.StringVar(&ssCert, "c", "", `https self-sign cert`)
 | |
| 	flag.StringVar(&ssCert, "ssc", "", `https self-sign cert`)
 | |
| 
 | |
| 	var oproxies Strings
 | |
| 	flag.Var(&oproxies, "p", "proxy ruler")
 | |
| 	flag.Var(&oproxies, "proxy", "one or more proxy the matched path to backend, for example, -proxy http://127.0.0.1:8888/api/webrtc")
 | |
| 
 | |
| 	var oprehooks Strings
 | |
| 	flag.Var(&oprehooks, "pre-hook", "the pre-hook ruler, with request")
 | |
| 
 | |
| 	var sdomains, skeys, scerts Strings
 | |
| 	flag.Var(&sdomains, "sdomain", "the SSL hostname")
 | |
| 	flag.Var(&skeys, "skey", "the SSL key for domain")
 | |
| 	flag.Var(&scerts, "scert", "the SSL cert for domain")
 | |
| 
 | |
| 	var trimSlashLimit int
 | |
| 	var noRedirectIndex, trimLastSlash bool
 | |
| 	flag.BoolVar(&noRedirectIndex, "no-redirect-index", false, "Whether serve with index.html without redirect.")
 | |
| 	flag.BoolVar(&trimLastSlash, "trim-last-slash", false, "Whether trim last slash by HTTP redirect(302).")
 | |
| 	flag.IntVar(&trimSlashLimit, "trim-slash-limit", 0, "Only trim last slash when got enough directories.")
 | |
| 
 | |
| 	flag.Usage = func() {
 | |
| 		fmt.Println(fmt.Sprintf("Usage: %v -t http -s https -d domains -r root -e cache -l lets -k ssk -c ssc -p proxy", os.Args[0]))
 | |
| 		fmt.Println(fmt.Sprintf("	"))
 | |
| 		fmt.Println(fmt.Sprintf("Options:"))
 | |
| 		fmt.Println(fmt.Sprintf("	-t, -http string"))
 | |
| 		fmt.Println(fmt.Sprintf("			Listen at port for HTTP server. Default: 0, disable HTTP."))
 | |
| 		fmt.Println(fmt.Sprintf("	-s, -https string"))
 | |
| 		fmt.Println(fmt.Sprintf("			Listen at port for HTTPS server. Default: 0, disable HTTPS."))
 | |
| 		fmt.Println(fmt.Sprintf("	-r, -root string"))
 | |
| 		fmt.Println(fmt.Sprintf("			The www root path. Supports relative to argv[0]=%v. Default: ./html", path.Dir(os.Args[0])))
 | |
| 		fmt.Println(fmt.Sprintf("	-no-redirect-index=bool"))
 | |
| 		fmt.Println(fmt.Sprintf("			Whether serve with index.html without redirect. Default: false"))
 | |
| 		fmt.Println(fmt.Sprintf("	-p, -proxy string"))
 | |
| 		fmt.Println(fmt.Sprintf("			Proxy path to backend. For example: http://127.0.0.1:8888/api/webrtc"))
 | |
| 		fmt.Println(fmt.Sprintf("			Proxy path to backend. For example: http://127.0.0.1:8888/api/webrtc?modifyRequestHost=false"))
 | |
| 		fmt.Println(fmt.Sprintf("			Proxy path to backend. For example: http://127.0.0.1:8888/api/webrtc?keepUpsreamServer=true"))
 | |
| 		fmt.Println(fmt.Sprintf("			Proxy path to backend. For example: http://127.0.0.1:8888/api/webrtc?trimPrefix=/ffmpeg"))
 | |
| 		fmt.Println(fmt.Sprintf("			Proxy path to backend. For example: http://127.0.0.1:8888/api/webrtc?addPrefix=/release"))
 | |
| 		fmt.Println(fmt.Sprintf("	-pre-hook string"))
 | |
| 		fmt.Println(fmt.Sprintf("			Pre-hook to backend, with request. For example: http://127.0.0.1:8888/api/stat"))
 | |
| 		fmt.Println(fmt.Sprintf("Options for HTTPS(letsencrypt cert):"))
 | |
| 		fmt.Println(fmt.Sprintf("	-l, -lets=bool"))
 | |
| 		fmt.Println(fmt.Sprintf("			Whether use letsencrypt CA. Default: false"))
 | |
| 		fmt.Println(fmt.Sprintf("	-e, -cache string"))
 | |
| 		fmt.Println(fmt.Sprintf("			The letsencrypt cache. Default: ./letsencrypt.cache"))
 | |
| 		fmt.Println(fmt.Sprintf("	-d, -domains string"))
 | |
| 		fmt.Println(fmt.Sprintf("			Set the validate HTTPS domain. For example: ossrs.net,www.ossrs.net"))
 | |
| 		fmt.Println(fmt.Sprintf("Options for HTTPS(file-based cert):"))
 | |
| 		fmt.Println(fmt.Sprintf("	-k, -ssk string"))
 | |
| 		fmt.Println(fmt.Sprintf("			The self-sign or validate file-based key file."))
 | |
| 		fmt.Println(fmt.Sprintf("	-c, -ssc string"))
 | |
| 		fmt.Println(fmt.Sprintf("			The self-sign or validate file-based cert file."))
 | |
| 		fmt.Println(fmt.Sprintf("	-sdomain string"))
 | |
| 		fmt.Println(fmt.Sprintf("			For multiple HTTPS site, the domain name. For example: ossrs.net"))
 | |
| 		fmt.Println(fmt.Sprintf("	-skey string"))
 | |
| 		fmt.Println(fmt.Sprintf("			For multiple HTTPS site, the key file."))
 | |
| 		fmt.Println(fmt.Sprintf("	-scert string"))
 | |
| 		fmt.Println(fmt.Sprintf("			For multiple HTTPS site, the cert file."))
 | |
| 		fmt.Println(fmt.Sprintf("For example:"))
 | |
| 		fmt.Println(fmt.Sprintf("	%v -t 8080 -s 9443 -r ./html", os.Args[0]))
 | |
| 		fmt.Println(fmt.Sprintf("	%v -t 8080 -s 9443 -r ./html -p http://ossrs.net:1985/api/v1/versions", os.Args[0]))
 | |
| 		fmt.Println(fmt.Sprintf("Generate cert for self-sign HTTPS:"))
 | |
| 		fmt.Println(fmt.Sprintf("	openssl genrsa -out server.key 2048"))
 | |
| 		fmt.Println(fmt.Sprintf(`	openssl req -new -x509 -key server.key -out server.crt -days 365 -subj "/C=CN/ST=Beijing/L=Beijing/O=Me/OU=Me/CN=me.org"`))
 | |
| 		fmt.Println(fmt.Sprintf("For example:"))
 | |
| 		fmt.Println(fmt.Sprintf("	%v -s 9443 -r ./html -sdomain ossrs.net -skey ossrs.net.key -scert ossrs.net.pem", os.Args[0]))
 | |
| 	}
 | |
| 	flag.Parse()
 | |
| 
 | |
| 	if useLetsEncrypt && len(httpsPorts) == 0 {
 | |
| 		return oe.Errorf("for letsencrypt, https=%v must be 0(disabled) or 443(enabled)", httpsPorts)
 | |
| 	}
 | |
| 	if len(httpPorts) == 0 && len(httpsPorts) == 0 {
 | |
| 		flag.Usage()
 | |
| 		os.Exit(-1)
 | |
| 	}
 | |
| 
 | |
| 	// If trim last slash, we should enable no redirect index, to avoid infinitely redirect.
 | |
| 	if trimLastSlash {
 | |
| 		noRedirectIndex = true
 | |
| 	}
 | |
| 	fmt.Println(fmt.Sprintf("Config trimLastSlash=%v, trimSlashLimit=%v, noRedirectIndex=%v", trimLastSlash, trimSlashLimit, noRedirectIndex))
 | |
| 
 | |
| 	var proxyUrls []*url.URL
 | |
| 	proxies := make(map[string]*url.URL)
 | |
| 	for _, oproxy := range []string(oproxies) {
 | |
| 		if oproxy == "" {
 | |
| 			return oe.Errorf("empty proxy in %v", oproxies)
 | |
| 		}
 | |
| 
 | |
| 		proxyUrl, err := url.Parse(oproxy)
 | |
| 		if err != nil {
 | |
| 			return oe.Wrapf(err, "parse proxy %v", oproxy)
 | |
| 		}
 | |
| 
 | |
| 		if _, ok := proxies[proxyUrl.Path]; ok {
 | |
| 			return oe.Errorf("proxy %v duplicated", proxyUrl.Path)
 | |
| 		}
 | |
| 
 | |
| 		proxyUrls = append(proxyUrls, proxyUrl)
 | |
| 		proxies[proxyUrl.Path] = proxyUrl
 | |
| 		ol.Tf(ctx, "Proxy %v to %v", proxyUrl.Path, oproxy)
 | |
| 	}
 | |
| 
 | |
| 	var preHookUrls []*url.URL
 | |
| 	preHooks := make(map[string]*url.URL)
 | |
| 	for _, oprehook := range []string(oprehooks) {
 | |
| 		if oprehook == "" {
 | |
| 			return oe.Errorf("empty pre-hook in %v", oprehooks)
 | |
| 		}
 | |
| 
 | |
| 		preHookUrl, err := url.Parse(oprehook)
 | |
| 		if err != nil {
 | |
| 			return oe.Wrapf(err, "parse pre-hook %v", oprehook)
 | |
| 		}
 | |
| 
 | |
| 		if _, ok := preHooks[preHookUrl.Path]; ok {
 | |
| 			return oe.Errorf("pre-hook %v duplicated", preHookUrl.Path)
 | |
| 		}
 | |
| 
 | |
| 		preHookUrls = append(preHookUrls, preHookUrl)
 | |
| 		preHooks[preHookUrl.Path] = preHookUrl
 | |
| 		ol.Tf(ctx, "pre-hook %v to %v", preHookUrl.Path, oprehook)
 | |
| 	}
 | |
| 
 | |
| 	if !path.IsAbs(cacheFile) && path.IsAbs(os.Args[0]) {
 | |
| 		cacheFile = path.Join(path.Dir(os.Args[0]), cacheFile)
 | |
| 	}
 | |
| 	if !path.IsAbs(html) && path.IsAbs(os.Args[0]) {
 | |
| 		html = path.Join(path.Dir(os.Args[0]), html)
 | |
| 	}
 | |
| 
 | |
| 	serveFileNoRedirect := func (w http.ResponseWriter, r *http.Request, name string) {
 | |
| 		upath := path.Join(html, path.Clean(r.URL.Path))
 | |
| 
 | |
| 		// Redirect without the last slash.
 | |
| 		if trimLastSlash && r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") {
 | |
| 			u := strings.TrimSuffix(r.URL.Path, "/")
 | |
| 			if r.URL.RawQuery != "" {
 | |
| 				u += "?" + r.URL.RawQuery
 | |
| 			}
 | |
| 			if strings.Count(u, "/") >= trimSlashLimit {
 | |
| 				http.Redirect(w, r, u, http.StatusFound)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Append the index.html path if access a directory.
 | |
| 		if noRedirectIndex && !strings.Contains(path.Base(upath), ".") {
 | |
| 			if d, err := os.Stat(upath); err != nil {
 | |
| 				http.Error(w, err.Error(), http.StatusInternalServerError)
 | |
| 				return
 | |
| 			} else if d.IsDir() {
 | |
| 				upath = path.Join(upath, "index.html")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		http.ServeFile(w, r, upath)
 | |
| 	}
 | |
| 
 | |
| 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | |
| 		oh.SetHeader(w)
 | |
| 
 | |
| 		if o := r.Header.Get("Origin"); len(o) > 0 {
 | |
| 			// SRS does not need cookie or credentials, so we disable CORS credentials, and use * for CORS origin,
 | |
| 			// headers, expose headers and methods.
 | |
| 			w.Header().Set("Access-Control-Allow-Origin", "*")
 | |
| 			// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
 | |
| 			w.Header().Set("Access-Control-Allow-Headers", "*")
 | |
| 			// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
 | |
| 			w.Header().Set("Access-Control-Allow-Methods", "*")
 | |
| 			// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
 | |
| 			w.Header().Set("Access-Control-Expose-Headers", "*")
 | |
| 			// https://stackoverflow.com/a/24689738/17679565
 | |
| 			// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
 | |
| 			w.Header().Set("Access-Control-Allow-Credentials", "false")
 | |
| 		}
 | |
| 
 | |
| 		// For matched OPTIONS, directly return without response.
 | |
| 		if r.Method == "OPTIONS" {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if proxyUrls == nil {
 | |
| 			if r.URL.Path == "/httpx/v1/versions" {
 | |
| 				oh.WriteVersion(w, r, Version())
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			serveFileNoRedirect(w, r, path.Join(html, path.Clean(r.URL.Path)))
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// Find pre-hook to serve with proxy.
 | |
| 		var preHook *url.URL
 | |
| 		for _, preHookUrl := range preHookUrls {
 | |
| 			if !shouldProxyURL(r.URL.Path, preHookUrl.Path) {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			if p, ok := preHooks[preHookUrl.Path]; ok {
 | |
| 				preHook = p
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Find proxy to serve it.
 | |
| 		for _, proxyUrl := range proxyUrls {
 | |
| 			if !shouldProxyURL(r.URL.Path, proxyUrl.Path) {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			if proxy, ok := proxies[proxyUrl.Path]; ok {
 | |
| 				p := NewComplexProxy(ctx, proxy, preHook, r)
 | |
| 				p.ServeHTTP(w, r)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		serveFileNoRedirect(w, r, path.Join(html, path.Clean(r.URL.Path)))
 | |
| 	})
 | |
| 
 | |
| 	var protos []string
 | |
| 	if len(httpPorts) > 0 {
 | |
| 		protos = append(protos, fmt.Sprintf("http(:%v)", strings.Join(httpPorts, ",")))
 | |
| 	}
 | |
| 	for _, httpsPort := range httpsPorts {
 | |
| 		if httpsPort == "0" {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		s := httpsDomains
 | |
| 		if httpsDomains == "" {
 | |
| 			s = "all domains"
 | |
| 		}
 | |
| 
 | |
| 		if useLetsEncrypt {
 | |
| 			protos = append(protos, fmt.Sprintf("https(:%v, %v, %v)", httpsPort, s, cacheFile))
 | |
| 		} else {
 | |
| 			protos = append(protos, fmt.Sprintf("https(:%v)", httpsPort))
 | |
| 		}
 | |
| 
 | |
| 		if useLetsEncrypt {
 | |
| 			protos = append(protos, "letsencrypt")
 | |
| 		} else if ssKey != "" {
 | |
| 			protos = append(protos, fmt.Sprintf("self-sign(%v, %v)", ssKey, ssCert))
 | |
| 		} else if len(sdomains) == 0 {
 | |
| 			return oe.New("no ssl config")
 | |
| 		}
 | |
| 
 | |
| 		for i := 0; i < len(sdomains); i++ {
 | |
| 			sdomain, skey, scert := sdomains[i], skeys[i], scerts[i]
 | |
| 			if f, err := os.Open(scert); err != nil {
 | |
| 				return oe.Wrapf(err, "open cert %v for %v err %+v", scert, sdomain, err)
 | |
| 			} else {
 | |
| 				f.Close()
 | |
| 			}
 | |
| 
 | |
| 			if f, err := os.Open(skey); err != nil {
 | |
| 				return oe.Wrapf(err, "open key %v for %v err %+v", skey, sdomain, err)
 | |
| 			} else {
 | |
| 				f.Close()
 | |
| 			}
 | |
| 			protos = append(protos, fmt.Sprintf("ssl(%v,%v,%v)", sdomain, skey, scert))
 | |
| 		}
 | |
| 	}
 | |
| 	ol.Tf(ctx, "%v html root at %v", strings.Join(protos, ", "), string(html))
 | |
| 
 | |
| 	if len(httpsPorts) > 0 && !useLetsEncrypt && ssKey != "" {
 | |
| 		if f, err := os.Open(ssCert); err != nil {
 | |
| 			return oe.Wrapf(err, "open cert %v err %+v", ssCert, err)
 | |
| 		} else {
 | |
| 			f.Close()
 | |
| 		}
 | |
| 
 | |
| 		if f, err := os.Open(ssKey); err != nil {
 | |
| 			return oe.Wrapf(err, "open key %v err %+v", ssKey, err)
 | |
| 		} else {
 | |
| 			f.Close()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	wg := sync.WaitGroup{}
 | |
| 	ctx, cancel := context.WithCancel(ctx)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	var httpServers []*http.Server
 | |
| 
 | |
| 	for _, v := range httpPorts {
 | |
| 		httpPort, err := strconv.ParseInt(v, 10, 64)
 | |
| 		if err != nil {
 | |
| 			return oe.Wrapf(err, "parse %v", v)
 | |
| 		}
 | |
| 
 | |
| 		wg.Add(1)
 | |
| 		go func(httpPort int) {
 | |
| 			defer wg.Done()
 | |
| 
 | |
| 			ctx = ol.WithContext(ctx)
 | |
| 			if httpPort == 0 {
 | |
| 				ol.W(ctx, "http server disabled")
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			defer cancel()
 | |
| 			hs := &http.Server{Addr: fmt.Sprintf(":%v", httpPort), Handler: nil}
 | |
| 			httpServers = append(httpServers, hs)
 | |
| 			ol.Tf(ctx, "http serve at %v", httpPort)
 | |
| 
 | |
| 			if err := hs.ListenAndServe(); err != nil {
 | |
| 				ol.Ef(ctx, "http serve err %+v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			ol.T("http server ok")
 | |
| 		}(int(httpPort))
 | |
| 	}
 | |
| 
 | |
| 	for _, v := range httpsPorts {
 | |
| 		httpsPort, err := strconv.ParseInt(v, 10, 64)
 | |
| 		if err != nil {
 | |
| 			return oe.Wrapf(err, "parse %v", v)
 | |
| 		}
 | |
| 
 | |
| 		wg.Add(1)
 | |
| 		go func() {
 | |
| 			defer wg.Done()
 | |
| 
 | |
| 			ctx = ol.WithContext(ctx)
 | |
| 			if httpsPort == 0 {
 | |
| 				ol.W(ctx, "https server disabled")
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			defer cancel()
 | |
| 
 | |
| 			var err error
 | |
| 			var m https.Manager
 | |
| 			if useLetsEncrypt {
 | |
| 				var domains []string
 | |
| 				if httpsDomains != "" {
 | |
| 					domains = strings.Split(httpsDomains, ",")
 | |
| 				}
 | |
| 
 | |
| 				if m, err = https.NewLetsencryptManager("", domains, cacheFile); err != nil {
 | |
| 					ol.Ef(ctx, "create letsencrypt manager err %+v", err)
 | |
| 					return
 | |
| 				}
 | |
| 			} else if ssKey != "" {
 | |
| 				if m, err = https.NewSelfSignManager(ssCert, ssKey); err != nil {
 | |
| 					ol.Ef(ctx, "create self-sign manager err %+v", err)
 | |
| 					return
 | |
| 				}
 | |
| 			} else if len(sdomains) > 0 {
 | |
| 				if m, err = NewCertsManager(sdomains, skeys, scerts); err != nil {
 | |
| 					ol.Ef(ctx, "create ssl managers err %+v", err)
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			hss := &http.Server{
 | |
| 				Addr: fmt.Sprintf(":%v", httpsPort),
 | |
| 				TLSConfig: &tls.Config{
 | |
| 					GetCertificate: m.GetCertificate,
 | |
| 				},
 | |
| 			}
 | |
| 			httpServers = append(httpServers, hss)
 | |
| 			ol.Tf(ctx, "https serve at %v", httpsPort)
 | |
| 
 | |
| 			if err = hss.ListenAndServeTLS("", ""); err != nil {
 | |
| 				ol.Ef(ctx, "https serve err %+v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			ol.T("https serve ok")
 | |
| 		}()
 | |
| 	}
 | |
| 
 | |
| 	select {
 | |
| 	case <-ctx.Done():
 | |
| 		for _, server := range httpServers {
 | |
| 			server.Close()
 | |
| 		}
 | |
| 	}
 | |
| 	wg.Wait()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	ctx := ol.WithContext(context.Background())
 | |
| 	if err := run(ctx); err != nil {
 | |
| 		ol.Ef(ctx, "run err %+v", err)
 | |
| 		os.Exit(-1)
 | |
| 	}
 | |
| }
 |