├── Dockerfile ├── README.md ├── etc ├── frps.ini.tpl └── service │ ├── acmeproxy │ ├── log │ │ └── run │ └── run │ ├── frps │ └── run │ ├── linknotifier │ └── run │ └── portmanager │ └── run ├── plugins ├── acmeproxy │ ├── api.go │ ├── go.mod │ ├── main.go │ └── proxy.go ├── linknotifier │ ├── example-notification-email.html.tpl │ ├── go.mod │ └── main.go └── portmanager │ ├── go.mod │ └── main.go └── start_runit /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | RUN apk --no-cache add build-base git gcc 4 | 5 | ENV FRP_VERSION 0.34.1 6 | 7 | RUN git clone https://github.com/fatedier/frp.git /frp && cd /frp && git reset --hard v${FRP_VERSION} 8 | RUN cd /frp && make 9 | 10 | ADD ./plugins /src 11 | RUN cd /src/portmanager && go build 12 | RUN cd /src/acmeproxy && go mod tidy && go build 13 | RUN cd /src/linknotifier && go mod tidy && go build 14 | 15 | FROM alpine:latest 16 | MAINTAINER luka.cehovin@gmail.com 17 | 18 | ENV GOTEMP_VERSION 3.5.0 19 | 20 | RUN apk add --no-cache wget ca-certificates tar runit 21 | 22 | RUN wget https://github.com/hairyhenderson/gomplate/releases/download/v${GOTEMP_VERSION}/gomplate_linux-amd64-slim -O /usr/local/bin/gotemp && \ 23 | chmod +x /usr/local/bin/gotemp 24 | 25 | COPY --from=build /frp/bin/frps /usr/local/bin/ 26 | COPY --from=build /src/portmanager/portmanager /usr/local/bin/ 27 | COPY --from=build /src/acmeproxy/acmeproxy /usr/local/bin/ 28 | COPY --from=build /src/linknotifier/linknotifier /usr/local/bin/ 29 | COPY start_runit /sbin/ 30 | COPY etc /etc/ 31 | 32 | VOLUME /data 33 | 34 | EXPOSE 80 443 7000 7001 7500 30000-30900 35 | 36 | CMD ["/sbin/start_runit"] 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker container for FRP server. 2 | 3 | The image includes two useful server plugins that can be enabled dynamically. The first one is PortManager that maintains a persistent mapping of proxy ports in case the server is restarted. This way the ports will not be redistributed even if the image is upgraded. The second plugin is ACMEProxy, it uses Let's Encrypt (or any other ACME based certificate authority) to automatically secure exposed HTTP connections and redirect them to HTTPS. 4 | 5 | Environment configuration: 6 | 7 | * `FRPS_BIND_ADDRESS` - bind to specific address, defaults to 0.0.0.0 8 | * `FRPS_DASHBOARD` - set to enable FRPS dashboard 9 | * `FRPS_DASHBOARD_ADDRESS` - bind dashboard to specific address, defaults to 0.0.0.0 10 | * `FRPS_DASHBOARD_USER` - username to access dashboard, defaults to "frpsadmin" 11 | * `FRPS_DASHBOARD_PASSWORD` - password to access dashboard, defaults to "frpsadmin" 12 | * `FRPS_AUTH_TOKEN` - token for clients, defaults to "abcdefghi" 13 | * `FRPS_MAX_PORTS` - max ports per client, defaults to unlimited 14 | * `FRPS_SUBDOMAIN_HOST` - subdomain for virtual hosts, defaults to "frps.com" 15 | * `FRPS_TCP_MUX` - TCP multiplexing, defaults to true 16 | * `FRPS_PERSISTENT_PORTS` - Enable to turn on PortManager plugin, defaults to false 17 | * `FRPS_LETSENCRYPT_EMAIL` - Set to your email to enable ACMEProxy, defaults to empty string 18 | * `FRPS_LINK_NOTIFIER` - Enable to turn on LinkNotifier plugin, defaults to false 19 | 20 | Note that an external volume has to be mounted to `/data` to make the port reservations and certificates persistent. 21 | 22 | ### LinkNotifier 23 | 24 | Plugin can notify user of its active/inactive proxy ports via email. The following information must be provided when starting docker: 25 | 26 | * `FRPS_LINK_NOTIFIER` - set environment var to enable the plugin 27 | * `FRPS_LINK_NOTIFIER_SMTP_SERVER`- set environment var to SMTP server in format `hostname.com:port` 28 | * `FRPS_LINK_NOTIFIER_SMTP_ACCOUNT` - set environment var to your email/account name 29 | * `FRPS_LINK_NOTIFIER_SMTP_PASS` - set environment var to your password 30 | * `FRPS_LINK_NOTIFIER_EMAIL_SUBJECT` - set environment var to subject of the email, defaults to "Reverse proxy links update" 31 | * `FRPS_LINK_NOTIFIER_DELAY_SEC`- set environment var to seconds of delay after last modification has been done befor sending notification, defaults to 15 32 | * `FRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC`- set environment var to seconds of sleep time in infinite loop, defaults to 5 33 | * `FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC` - set environment var to second of timeout after port connection is considered inactive, default to 2 34 | 35 | Template of the email must be provided in `/data/notification_email.html.tpl`. Template is run for each email notification and passes `DisplayProxyList` struct, which groups proxies of the same user. `Active` and `Inactive` group proxies with the same ContainerName for the given user. 36 | 37 | ```go 38 | 39 | type DisplayProxyList struct { 40 | Active map[string][]ProxyInfo `json:"active"` 41 | Inactive map[string][]ProxyInfo `json:"inactive"` 42 | } 43 | 44 | type ProxyInfo struct { 45 | Name string `json:"name"` 46 | ContainerName string `json:"container_name"` 47 | ProxyType string `json:"proxy_type"` 48 | RemotePort int `json:"remote_port"` 49 | LocalPort int `json:"local_port"` 50 | Email string `json:"email"` 51 | ClientPrefix string `json:"frps_prefix"` 52 | Url string `json:"url"` 53 | Active bool `json:"active"` 54 | Notified bool `json:"notified"` 55 | 56 | } 57 | ``` 58 | 59 | To activate the email notification, FRP client must provide the following meta data in its configuration for each proxy connection: 60 | * `meta_notify_email` - set to email address that will recieve the notificaiton 61 | * `meta_local_port` - set to local port used (the same as local_port) 62 | * `meta_frpc_prefix` - set to FRP client specific name (e.g., server hostname) 63 | -------------------------------------------------------------------------------- /etc/frps.ini.tpl: -------------------------------------------------------------------------------- 1 | [common] 2 | bind_addr = {{getenv "FRPS_BIND_ADDRESS" "0.0.0.0"}} 3 | bind_port = 7000 4 | 5 | # udp port to help make udp hole to penetrate nat 6 | bind_udp_port = 7001 7 | 8 | # udp port used for kcp protocol, it can be same with 'bind_port' 9 | # if not set, kcp is disabled in frps 10 | kcp_bind_port = 7000 11 | 12 | # specify which address proxy will listen for, default value is same with bind_addr 13 | # proxy_bind_addr = 127.0.0.1 14 | 15 | # if you want to support virtual host, you must set the http port for listening (optional) 16 | # Note: http port and https port can be same with bind_port 17 | vhost_http_port = 80 18 | vhost_https_port = 443 19 | 20 | # response header timeout(seconds) for vhost http server, default is 60s 21 | # vhost_http_timeout = 60 22 | 23 | {{if env.Getenv "FRPS_DASHBOARD" }} 24 | dashboard_addr = {{getenv "FRPS_DASHBOARD_ADDRESS" "0.0.0.0"}} 25 | dashboard_port = 7500 26 | 27 | # dashboard user and passwd for basic auth protect, if not set, both default value is admin 28 | dashboard_user = {{getenv "FRPS_DASHBOARD_USER" "frpsadmin"}} 29 | dashboard_pwd = {{getenv "FRPS_DASHBOARD_PASSWORD" "frpsadmin"}} 30 | {{end}} 31 | 32 | {{if env.Getenv "FRPS_LOGFILE" }} 33 | log_file = {{getenv "FRPS_LOGFILE" "/var/log/frps.log"}} 34 | 35 | # trace, debug, info, warn, error 36 | log_level = {{getenv "FRPS_LOG_LEVEL" "warn"}} 37 | 38 | log_max_days = {{getenv "FRPS_LOG_DAYS" "5"}} 39 | {{end}} 40 | 41 | token = {{getenv "FRPS_AUTH_TOKEN" "abcdefghi"}} 42 | allow_ports = 30000-30900 43 | 44 | # pool_count in each proxy will change to max_pool_count if they exceed the maximum value 45 | max_pool_count = 5 46 | 47 | max_ports_per_client = {{getenv "FRPS_MAX_PORTS" "0"}} 48 | 49 | subdomain_host = {{getenv "FRPS_SUBDOMAIN_HOST" "frps.com"}} 50 | 51 | tcp_mux = {{getenv "FRPS_TCP_MUX" "true"}} 52 | 53 | {{if env.Getenv "FRPS_PERSISTENT_PORTS" }} 54 | [plugin.0-port-manager] 55 | addr = 127.0.0.1:9001 56 | path = /ports 57 | ops = NewProxy 58 | {{end}} 59 | 60 | {{if env.Getenv "FRPS_LETSENCRYPT_EMAIL" }} 61 | [plugin.1-acme-manager] 62 | addr = 127.0.0.1:9002 63 | path = /acme 64 | ops = NewProxy 65 | {{end}} 66 | 67 | {{if env.Getenv "FRPS_LINK_NOTIFIER" }} 68 | [plugin.2-linknotifier] 69 | addr = 127.0.0.1:9003 70 | path = /notifier 71 | ops = NewProxy 72 | {{end}} 73 | -------------------------------------------------------------------------------- /etc/service/acmeproxy/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p `dirname $FRPS_LOGFILE`/acmeproxy 4 | 5 | exec svlogd -tt `dirname $FRPS_LOGFILE`/acmeproxy 6 | -------------------------------------------------------------------------------- /etc/service/acmeproxy/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cd /data 4 | 5 | if [ ! -z "${FRPS_LETSENCRYPT_EMAIL}" ]; then 6 | 7 | exec /usr/local/bin/acmeproxy --api 9002 --domain ${FRPS_SUBDOMAIN_HOST:-example.com} 2>&1 8 | 9 | else 10 | 11 | sv stop acmeproxy 12 | 13 | fi 14 | -------------------------------------------------------------------------------- /etc/service/frps/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | /usr/local/bin/gotemp -f /etc/frps.ini.tpl -o /etc/frps.ini 4 | 5 | sv start portmanager || exit 1 6 | 7 | if [ ! -z "${FRPS_LETSENCRYPT_EMAIL}" ]; then 8 | 9 | sv start acmeproxy || exit 1 10 | 11 | fi 12 | 13 | exec /usr/local/bin/frps -c /etc/frps.ini 2>&1 14 | -------------------------------------------------------------------------------- /etc/service/linknotifier/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cd /data 4 | 5 | export PLUGIN_PORT=9003 6 | 7 | exec /usr/local/bin/linknotifier 2>&1 8 | -------------------------------------------------------------------------------- /etc/service/portmanager/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cd /data 4 | 5 | export PLUGIN_PORT=9001 6 | 7 | exec /usr/local/bin/portmanager 2>&1 8 | -------------------------------------------------------------------------------- /plugins/acmeproxy/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | type Request struct { 12 | Version string `json:"version"` 13 | Op string `json:"op"` 14 | Content map[string]interface{} `json:"content"` 15 | } 16 | 17 | type Response struct { 18 | Reject bool `json:"reject"` 19 | RejectReason string `json:"reject_reason"` 20 | Unchange bool `json:"unchange"` 21 | Content map[string]interface{} `json:"content"` 22 | } 23 | 24 | type DomainInfo struct { 25 | passthrough bool 26 | } 27 | 28 | func check(e error) { 29 | if e != nil { 30 | panic(e) 31 | } 32 | } 33 | 34 | type APIServer struct { 35 | logger *log.Logger 36 | proxy *ProxyServer 37 | domain string 38 | mutex sync.RWMutex 39 | } 40 | 41 | func (s APIServer) handler(w http.ResponseWriter, r *http.Request) { 42 | 43 | switch r.Method { 44 | case "POST": 45 | d := json.NewDecoder(r.Body) 46 | r := &Request{} 47 | o := &Response{} 48 | err := d.Decode(r) 49 | if err != nil { 50 | http.Error(w, err.Error(), http.StatusInternalServerError) 51 | } 52 | 53 | if r.Op != "NewProxy" { 54 | w.WriteHeader(http.StatusMethodNotAllowed) 55 | fmt.Fprintf(w, "Not allowed.") 56 | return 57 | } 58 | 59 | o.Reject = false 60 | o.Unchange = true 61 | 62 | if r.Content["proxy_type"] == "http" || r.Content["proxy_type"] == "https" { 63 | if r.Content["subdomain"] != "" { 64 | var full_domain = r.Content["subdomain"].(string) + "." + s.domain 65 | s.proxy.addFrontend(full_domain, r.Content["proxy_type"] == "https") 66 | } 67 | 68 | if r.Content["custom_domains"] != nil { 69 | 70 | for _, domain := range r.Content["custom_domains"].([]string) { 71 | s.proxy.addFrontend(domain, r.Content["proxy_type"] == "https") 72 | 73 | } 74 | } 75 | 76 | } 77 | 78 | 79 | js, err := json.Marshal(o) 80 | if err != nil { 81 | http.Error(w, err.Error(), http.StatusInternalServerError) 82 | return 83 | } 84 | w.Header().Set("Content-Type", "application/json") 85 | w.Write(js) 86 | 87 | default: 88 | w.WriteHeader(http.StatusMethodNotAllowed) 89 | fmt.Fprintf(w, "Not allowed.") 90 | } 91 | } 92 | 93 | func createAPIServer(logger *log.Logger, proxy *ProxyServer, port int, domain string) *APIServer { 94 | 95 | api := &APIServer{ 96 | logger: logger, 97 | proxy: proxy, 98 | domain: domain, 99 | } 100 | 101 | http.HandleFunc("/", api.handler) 102 | 103 | go func () { 104 | log.Println(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) 105 | }() 106 | 107 | return api 108 | 109 | } 110 | 111 | -------------------------------------------------------------------------------- /plugins/acmeproxy/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker-frps/plugins/acmeproxy 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b 7 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 8 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 9 | golang.org/x/text v0.3.2 10 | ) 11 | -------------------------------------------------------------------------------- /plugins/acmeproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "golang.org/x/crypto/acme/autocert" 8 | "net/http" 9 | "flag" 10 | "strings" 11 | ) 12 | 13 | func getEnvString(key string, def string) string { 14 | val, ok := os.LookupEnv(key) 15 | if !ok { 16 | return def 17 | } else { 18 | return val 19 | } 20 | } 21 | 22 | func redirectHttps(w http.ResponseWriter, r *http.Request){ 23 | host := strings.Split(r.Host, ":")[0] 24 | u := r.URL 25 | u.Host = host 26 | u.Scheme="https" 27 | log.Println(u.String()) 28 | http.Redirect(w,r,u.String(), http.StatusMovedPermanently) 29 | } 30 | 31 | func main() { 32 | 33 | var apiPort = flag.Int("api", 9000, "API port on localhost") 34 | var mainDomain = flag.String("domain", "", "Main domain for subdomains") 35 | 36 | flag.Parse() 37 | 38 | logger := log.New(os.Stdout, "acmeproxy ", log.LstdFlags|log.Lshortfile) 39 | 40 | m := &autocert.Manager{ 41 | Cache: autocert.DirCache("certs"), 42 | Prompt: autocert.AcceptTOS, 43 | Email: os.Getenv("FRPS_LETSENCRYPT_EMAIL"), 44 | } 45 | 46 | s := &ProxyServer{ 47 | Logger: logger, 48 | Manager: m, 49 | } 50 | 51 | createAPIServer(logger, s, *apiPort, *mainDomain) 52 | 53 | err := s.Run() 54 | if err != nil { 55 | fmt.Fprintf(os.Stderr, "Failed to start server: %v\n", err) 56 | os.Exit(1) 57 | } 58 | 59 | http.ListenAndServe(":81", m.HTTPHandler(http.HandlerFunc(redirectHttps))) 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /plugins/acmeproxy/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | vhost "github.com/inconshreveable/go-vhost" 6 | "golang.org/x/crypto/acme/autocert" 7 | "io" 8 | "log" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | const ( 15 | muxTimeout = 10 * time.Second 16 | defaultConnectTimeout = 10000 // milliseconds 17 | ) 18 | 19 | type ProxyServer struct { 20 | *log.Logger 21 | *autocert.Manager 22 | // these are for easier testing 23 | mux *vhost.TLSMuxer 24 | ready chan int 25 | } 26 | 27 | func (s *ProxyServer) Run() error { 28 | // bind a port to handle TLS connections 29 | l, err := net.Listen("tcp", ":444") 30 | if err != nil { 31 | return err 32 | } 33 | s.Printf("Serving connections on %v", l.Addr()) 34 | 35 | // start muxing on it 36 | s.mux, err = vhost.NewTLSMuxer(l, muxTimeout) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // custom error handler so we can log errors 42 | go func() { 43 | for { 44 | conn, err := s.mux.NextError() 45 | 46 | if conn == nil { 47 | s.Printf("Failed to mux next connection, error: %v", err) 48 | if _, ok := err.(vhost.Closed); ok { 49 | return 50 | } else { 51 | continue 52 | } 53 | } 54 | } 55 | }() 56 | 57 | // we're ready, signal it for testing 58 | if s.ready != nil { 59 | close(s.ready) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (s *ProxyServer) addFrontend(name string, passthrough bool) (err error) { 66 | fl, err := s.mux.Listen(name) 67 | if err != nil { 68 | return err 69 | } 70 | if passthrough { 71 | go s.runFrontend(name, nil, fl) 72 | } else { 73 | tlsconfig := &tls.Config{ 74 | GetCertificate: s.Manager.GetCertificate, 75 | NextProtos: []string{"http/1.1"}, 76 | } 77 | 78 | go s.runFrontend(name, tlsconfig, fl) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (s *ProxyServer) runFrontend(name string, tlsConfig *tls.Config, l net.Listener) { 85 | 86 | for { 87 | // accept next connection to this frontend 88 | conn, err := l.Accept() 89 | if err != nil { 90 | s.Printf("Failed to accept new connection for '%v': %v", conn.RemoteAddr()) 91 | if e, ok := err.(net.Error); ok { 92 | if e.Temporary() { 93 | continue 94 | } 95 | } 96 | return 97 | } 98 | 99 | // proxy the connection to an backend 100 | go s.proxyConnection(conn, tlsConfig) 101 | } 102 | } 103 | 104 | func (s *ProxyServer) proxyConnection(c net.Conn, tlsConfig *tls.Config) (err error) { 105 | var backend string 106 | // unwrap if tls cert/key was specified 107 | if tlsConfig != nil { 108 | var tlsc = tls.Server(c, tlsConfig) 109 | tlsc.Handshake() 110 | c = net.Conn(tlsc) 111 | backend = "localhost:80" 112 | } else { 113 | backend = "localhost:443" 114 | } 115 | 116 | // dial the backend 117 | upConn, err := net.DialTimeout("tcp", backend, time.Duration(defaultConnectTimeout)*time.Millisecond) 118 | if err != nil { 119 | s.Printf("Failed to dial backend connection %v: %v", backend, err) 120 | c.Close() 121 | return 122 | } 123 | 124 | // join the connections 125 | s.joinConnections(c, upConn) 126 | return 127 | } 128 | 129 | func (s *ProxyServer) joinConnections(c1 net.Conn, c2 net.Conn) { 130 | var wg sync.WaitGroup 131 | halfJoin := func(dst net.Conn, src net.Conn) { 132 | defer wg.Done() 133 | defer dst.Close() 134 | defer src.Close() 135 | n, err := io.Copy(dst, src) 136 | if err != nil { 137 | s.Printf("Copy from %v to %v failed after %d bytes with error %v", src.RemoteAddr(), dst.RemoteAddr(), n, err) 138 | } 139 | } 140 | 141 | wg.Add(2) 142 | go halfJoin(c1, c2) 143 | go halfJoin(c2, c1) 144 | wg.Wait() 145 | } 146 | 147 | -------------------------------------------------------------------------------- /plugins/linknotifier/example-notification-email.html.tpl: -------------------------------------------------------------------------------- 1 | This is an automated message from auto-notifier that manages reverse proxy connections to docker containers. URLs and ports to containers may have been updated. 2 | 3 | You now have the following ACTIVE containers: 4 | {{ range $n, $p_list := .Active }} 5 | {{$n -}}: 6 | {{- range $key, $p:= $p_list }} 7 | {{$p.Url}} -> local port {{$p.LocalPort}} (on GPU server '{{ $p.ClientPrefix }}') {{ if not $p.Notified }}**NEW**{{end}} 8 | {{- end }} 9 | {{- end }} 10 | 11 | The following connections to containers are NOT active: 12 | {{ range $n, $p_list := .Inactive }} 13 | {{$n -}}: 14 | {{- range $key, $p:= $p_list }} 15 | {{$p.Url}} -> local port {{$p.LocalPort}} (on GPU server '{{ $p.ClientPrefix }}') 16 | {{- end }} 17 | {{- end }} 18 | 19 | Non-active container connections may be due to stopped/removed container, or container is started but the service at the local port inside the container is not responsive. 20 | 21 | For any question, please email cluster maintainers: custer-manager@mydomain.com 22 | 23 | -------------------------------------------------------------------------------- /plugins/linknotifier/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker-frps/plugins/linknotifier 2 | 3 | go 1.12 4 | 5 | -------------------------------------------------------------------------------- /plugins/linknotifier/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "encoding/json" 7 | "fmt" 8 | "errors" 9 | "net" 10 | "net/http" 11 | "strconv" 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/google/go-cmp/cmp/cmpopts" 14 | "io/ioutil" 15 | "time" 16 | "strings" 17 | "text/template" 18 | "net/smtp" 19 | "bytes" 20 | "sort" 21 | ) 22 | 23 | type Request struct { 24 | Version string `json:"version"` 25 | Op string `json:"op"` 26 | Content map[string]interface{} `json:"content"` 27 | } 28 | 29 | 30 | type Response struct { 31 | Reject bool `json:"reject"` 32 | RejectReason string `json:"reject_reason"` 33 | Unchange bool `json:"unchange"` 34 | Content map[string]interface{} `json:"content"` 35 | } 36 | 37 | type ProxyInfo struct { 38 | Name string `json:"name"` 39 | ContainerName string `json:"container_name"` 40 | ProxyType string `json:"proxy_type"` 41 | RemotePort int `json:"remote_port"` 42 | LocalPort int `json:"local_port"` 43 | Email string `json:"email"` 44 | ClientPrefix string `json:"frps_prefix"` 45 | Url string `json:"url"` 46 | Active bool `json:"active"` 47 | Notified bool `json:"notified"` 48 | 49 | } 50 | 51 | type ProxyList struct { 52 | Proxies map[string]ProxyInfo `json:"proxies"` 53 | } 54 | 55 | type DisplayProxyList struct { 56 | Active map[string][]ProxyInfo `json:"active"` 57 | Inactive map[string][]ProxyInfo `json:"inactive"` 58 | } 59 | 60 | type SortedProxyInfo []ProxyInfo 61 | 62 | var mutex sync.RWMutex 63 | var references = ProxyList{Proxies: make(map[string]ProxyInfo)} 64 | 65 | func getEnv(name string, def string) string { 66 | val := os.Getenv(name) 67 | 68 | if val == "" { 69 | return def 70 | } 71 | 72 | return val 73 | 74 | } 75 | 76 | func getEnvInt(name string, def int) int { 77 | val := os.Getenv(name) 78 | 79 | if val == "" { 80 | return def 81 | } 82 | 83 | ival, _ := strconv.ParseInt(val, 10, 32) 84 | 85 | return int(ival) 86 | } 87 | 88 | func (p SortedProxyInfo) Len() int { 89 | return len(p) 90 | } 91 | func (p SortedProxyInfo) Swap(i, j int) { 92 | p[i], p[j] = p[j], p[i] 93 | } 94 | func (p SortedProxyInfo) Less(i, j int) bool { 95 | // sort by LocalPort then by ClientPrefix 96 | if p[i].LocalPort == p[j].LocalPort { 97 | return p[i].ClientPrefix < p[j].ClientPrefix 98 | } else { 99 | return p[i].LocalPort < p[j].LocalPort 100 | } 101 | } 102 | 103 | func check(e error) { 104 | if e != nil { 105 | panic(e) 106 | } 107 | } 108 | func loadProxyLinksJSON() { 109 | file, err := ioutil.ReadFile("links.json") 110 | 111 | if err == nil { 112 | err = json.Unmarshal([]byte(file), &references) 113 | } 114 | } 115 | func saveProxyLinksJSON() { 116 | file, _ := json.MarshalIndent(references, "", " ") 117 | 118 | _ = ioutil.WriteFile("links.json", file, 0644) 119 | } 120 | 121 | func handler(w http.ResponseWriter, r *http.Request) { 122 | 123 | switch r.Method { 124 | case "POST": 125 | 126 | d := json.NewDecoder(r.Body) 127 | r := &Request{} 128 | o := &Response{} 129 | err := d.Decode(r) 130 | if err != nil { 131 | http.Error(w, err.Error(), http.StatusInternalServerError) 132 | } 133 | 134 | 135 | if r.Op != "NewProxy" { 136 | w.WriteHeader(http.StatusMethodNotAllowed) 137 | fmt.Fprintf(w, "Not allowed.") 138 | return 139 | } 140 | 141 | metas, ok := r.Content["metas"] 142 | 143 | // do nothing if metas does not exists 144 | if ok && metas != nil { 145 | 146 | metas := metas.(map[string]interface{}) 147 | 148 | notify_email, ok_email := metas["notify_email"] 149 | frpc_prefix, ok_prefix := metas["frpc_prefix"] 150 | local_port, ok_port := metas["local_port"] 151 | 152 | // do nothing if there is no notify_email, frpc_prefix or local_port in metas 153 | 154 | if ok_email && ok_prefix && ok_port { 155 | 156 | var key = fmt.Sprintf("%v:%v", r.Content["proxy_name"], r.Content["proxy_type"]) 157 | 158 | var url string = getEnv("FRPS_SUBDOMAIN_HOST","example.com") 159 | var remote_port int = 0 160 | 161 | if r.Content["proxy_type"] == "tcp" || r.Content["proxy_type"] == "udp" { 162 | remote_port = int(r.Content["remote_port"].(float64)) 163 | 164 | url = fmt.Sprintf("%v:%v", url, remote_port) 165 | 166 | } else if r.Content["proxy_type"] == "http" { 167 | remote_port = 80 168 | 169 | url = fmt.Sprintf("http://%v.%v", r.Content["subdomain"], url) 170 | 171 | } else if r.Content["proxy_type"] == "https" { 172 | remote_port = 443 173 | 174 | url = fmt.Sprintf("https://%v.%v", r.Content["subdomain"], url) 175 | 176 | } 177 | 178 | local_port, _ := strconv.Atoi(local_port.(string)) 179 | 180 | var container_name string = r.Content["proxy_name"].(string) 181 | 182 | // to get actual container name by removing prefix name and port number suffix 183 | container_name = strings.Replace(container_name, fmt.Sprintf("%s_",frpc_prefix),"", 1) 184 | container_name = strings.Replace(container_name, fmt.Sprintf("_%d",local_port), "", 1) 185 | 186 | ref := ProxyInfo{ Name: key, 187 | ContainerName: container_name, 188 | ProxyType: r.Content["proxy_type"].(string), 189 | RemotePort: remote_port, 190 | LocalPort: int(local_port), 191 | Email: notify_email.(string), 192 | ClientPrefix: frpc_prefix.(string), 193 | Url: url, 194 | Active: true, 195 | Notified: false } 196 | 197 | mutex.Lock() 198 | 199 | // first load proxy list from JSON file to sync with any changes done manually by the user 200 | loadProxyLinksJSON() 201 | 202 | link, ok := references.Proxies[key] 203 | 204 | if !ok || !cmp.Equal(link, ref, cmpopts.IgnoreFields(ProxyInfo{}, "Notified")) { 205 | // update reference if 206 | // - does not exists at all 207 | // - already exists but is not the same 208 | references.Proxies[key] = ref 209 | 210 | // save new links 211 | saveProxyLinksJSON() 212 | 213 | } 214 | 215 | mutex.Unlock() 216 | } 217 | } 218 | 219 | o.Reject = false 220 | o.Unchange = true 221 | 222 | js, err := json.Marshal(o) 223 | if err != nil { 224 | http.Error(w, err.Error(), http.StatusInternalServerError) 225 | return 226 | } 227 | w.Header().Set("Content-Type", "application/json") 228 | w.Write(js) 229 | 230 | default: 231 | w.WriteHeader(http.StatusMethodNotAllowed) 232 | fmt.Fprintf(w, "I can't do that.") 233 | } 234 | } 235 | 236 | // validateLine checks to see if a line has CR or LF as per RFC 5321 237 | 238 | func validateLine(line string) error { 239 | 240 | if strings.ContainsAny(line, "\n\r") { 241 | 242 | return errors.New("smtp: A line must not contain CR or LF") 243 | 244 | } 245 | 246 | return nil 247 | 248 | } 249 | 250 | func SendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error { 251 | 252 | if err := validateLine(from); err != nil { 253 | 254 | return err 255 | 256 | } 257 | 258 | for _, recp := range to { 259 | 260 | if err := validateLine(recp); err != nil { 261 | 262 | return err 263 | 264 | } 265 | 266 | } 267 | 268 | c, err := smtp.Dial(addr) 269 | 270 | if err != nil { 271 | 272 | return err 273 | 274 | } 275 | 276 | defer c.Close() 277 | 278 | if err = c.Hello("frps"); err != nil { 279 | 280 | return err 281 | 282 | } 283 | 284 | if a != nil { 285 | 286 | if err = c.Auth(a); err != nil { 287 | 288 | return err 289 | 290 | } 291 | 292 | } 293 | 294 | if err = c.Mail(from); err != nil { 295 | 296 | return err 297 | 298 | } 299 | 300 | for _, addr := range to { 301 | 302 | if err = c.Rcpt(addr); err != nil { 303 | 304 | return err 305 | 306 | } 307 | 308 | } 309 | 310 | w, err := c.Data() 311 | 312 | if err != nil { 313 | 314 | return err 315 | 316 | } 317 | 318 | _, err = w.Write(msg) 319 | 320 | if err != nil { 321 | 322 | return err 323 | 324 | } 325 | 326 | err = w.Close() 327 | 328 | if err != nil { 329 | 330 | return err 331 | 332 | } 333 | 334 | return c.Quit() 335 | 336 | } 337 | 338 | func notifier_main() { 339 | 340 | //var frps_subdomain_host string = getEnv("FRPS_SUBDOMAIN_HOST","example.com") 341 | 342 | // infinite loop that 343 | // - checks for changes of the links.json 344 | // - if at least 30 sec since last change 345 | var last_notified = time.Now() 346 | 347 | var FRPS_LINK_NOTIFIER_DELAY_SEC int = getEnvInt("FRPS_LINK_NOTIFIER_DELAY_SEC", 15) 348 | var FRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC int = getEnvInt("FRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC", 5) 349 | var FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC int = getEnvInt("FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC", 2) 350 | 351 | var FRPS_LINK_NOTIFIER_EMAIL_SUBJECT string = getEnv("FRPS_LINK_NOTIFIER_EMAIL_SUBJECT", "Reverse proxy links update") 352 | 353 | var FRPS_LINK_NOTIFIER_SMTP_ACCOUNT string = getEnv("FRPS_LINK_NOTIFIER_SMTP_ACCOUNT", "") 354 | var FRPS_LINK_NOTIFIER_SMTP_PASS string = getEnv("FRPS_LINK_NOTIFIER_SMTP_PASS", "") 355 | var FRPS_LINK_NOTIFIER_SMTP_SERVER string = getEnv("FRPS_LINK_NOTIFIER_SMTP_SERVER", "") 356 | 357 | tpl, err := template.ParseFiles("notification_email.html.tpl") 358 | 359 | if err != nil { 360 | fmt.Println("[linknotifier]: ERROR in notifier_main(): missing notification_email.html.tpl template file. E-mail notification will not be performed !!") 361 | return 362 | } 363 | 364 | var auth smtp.Auth 365 | 366 | if FRPS_LINK_NOTIFIER_SMTP_ACCOUNT != "" { 367 | 368 | auth := smtp.PlainAuth("", FRPS_LINK_NOTIFIER_SMTP_ACCOUNT, FRPS_LINK_NOTIFIER_SMTP_PASS, strings.Split(FRPS_LINK_NOTIFIER_SMTP_SERVER, ":")[0]) 369 | 370 | if auth == nil { 371 | fmt.Printf("[linknotifier]: ERROR in notifier_main(): server authentication failed (%s)\n", FRPS_LINK_NOTIFIER_SMTP_SERVER) 372 | return 373 | } 374 | 375 | } 376 | 377 | fmt.Printf("[linknotifier]: Started notification loop with:\n" + 378 | "[linknotifier]: \tFRPS_LINK_NOTIFIER_DELAY_SEC=%d\n" + 379 | "[linknotifier]: \tFRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC=%d\n" + 380 | "[linknotifier]: \tFRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC=%d\n" + 381 | "[linknotifier]: \tFRPS_LINK_NOTIFIER_SMTP_SERVER=%s\n" + 382 | "[linknotifier]: \tFRPS_LINK_NOTIFIER_SMTP_ACCOUNT=%s\n", 383 | FRPS_LINK_NOTIFIER_DELAY_SEC, FRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC, FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC, FRPS_LINK_NOTIFIER_SMTP_SERVER, FRPS_LINK_NOTIFIER_SMTP_ACCOUNT) 384 | 385 | for { 386 | file, err := os.Stat("links.json") 387 | if err == nil { 388 | 389 | modified_time := file.ModTime() 390 | 391 | if modified_time.After(last_notified) && time.Now().After(modified_time.Add(time.Duration(FRPS_LINK_NOTIFIER_DELAY_SEC) * time.Second)) { 392 | 393 | 394 | fmt.Printf("[linknotifier]: At least %d sec since last modification .. doing notification now\n", FRPS_LINK_NOTIFIER_DELAY_SEC) 395 | 396 | mutex.Lock() 397 | 398 | // load proxy links from JSON file to sync with any changes done manually by the user 399 | loadProxyLinksJSON() 400 | 401 | // first check for validity of each connection and flag unresponsive ones 402 | should_notify := false 403 | num_active := 0 404 | 405 | for name, _ := range references.Proxies { 406 | proxy_ref := references.Proxies[name] 407 | 408 | // check if connection is active 409 | proxy_ref.Active = check_connection(proxy_ref, FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC) 410 | 411 | if proxy_ref.Active { 412 | num_active = num_active + 1 413 | } 414 | 415 | // should notify only if any proxy is active and has not been yet notified 416 | should_notify = should_notify || (proxy_ref.Active && !proxy_ref.Notified) 417 | 418 | // update value 419 | references.Proxies[name] = proxy_ref 420 | 421 | } 422 | 423 | // save updated links 424 | saveProxyLinksJSON() 425 | 426 | // then group references by email notifications 427 | var gruped_proxies = make(map[string][]ProxyInfo) 428 | for _, proxy_ref := range references.Proxies { 429 | gruped_proxies[proxy_ref.Email] = append(gruped_proxies[proxy_ref.Email], proxy_ref) 430 | } 431 | 432 | // perform user notification if needed 433 | if should_notify { 434 | 435 | var num_sent_emails int = 0 436 | var email_recipients []string 437 | 438 | // go over each group and create a notification list 439 | for email, proxy_ref_list := range gruped_proxies { 440 | 441 | should_notify := false 442 | 443 | // first chech if any of the users connections have not been yet notified 444 | for _, proxy_ref := range proxy_ref_list { 445 | should_notify = should_notify || ! proxy_ref.Notified 446 | } 447 | 448 | // do notification only if user has not been notified for at least one connection 449 | if should_notify { 450 | 451 | var display_proxy_list = DisplayProxyList{Active: make(map[string][]ProxyInfo), 452 | Inactive: make(map[string][]ProxyInfo)} 453 | 454 | // get active connections first 455 | for _, proxy_ref := range proxy_ref_list { 456 | if proxy_ref.Active { 457 | // copy to list of active proxies for notification mail 458 | display_proxy_list.Active[proxy_ref.ContainerName] = append(display_proxy_list.Active[proxy_ref.ContainerName], proxy_ref) 459 | } 460 | } 461 | 462 | // get inactive connections last 463 | for _, proxy_ref := range proxy_ref_list { 464 | if !proxy_ref.Active { 465 | // copy to list of active proxies for notification mail 466 | display_proxy_list.Inactive[proxy_ref.ContainerName] = append(display_proxy_list.Inactive[proxy_ref.ContainerName], proxy_ref) 467 | } 468 | } 469 | // sort both active and inactive lists 470 | for k, _ := range display_proxy_list.Active { 471 | sort.Sort(SortedProxyInfo(display_proxy_list.Active[k])) 472 | } 473 | 474 | for k, _ := range display_proxy_list.Inactive { 475 | sort.Sort(SortedProxyInfo(display_proxy_list.Inactive[k])) 476 | } 477 | 478 | 479 | var msg bytes.Buffer 480 | err = tpl.Execute(&msg, display_proxy_list) 481 | 482 | if err != nil { 483 | fmt.Printf("[linknotifier]: ERROR %s", err) 484 | return 485 | } 486 | 487 | var msg_str string = fmt.Sprintf("To: %s\r\n", email) + 488 | fmt.Sprintf("Subject: %s\r\n", FRPS_LINK_NOTIFIER_EMAIL_SUBJECT ) + 489 | "\r\n" + 490 | fmt.Sprintf("%s\r\n",msg.String()) 491 | 492 | err := SendMail(FRPS_LINK_NOTIFIER_SMTP_SERVER, auth, FRPS_LINK_NOTIFIER_SMTP_ACCOUNT, []string{email}, []byte(msg_str)) 493 | 494 | if err != nil { 495 | fmt.Printf("[linknotifier]: ERROR in notifier_main(): when sending mail to %s got '%s'\n", email, err) 496 | continue 497 | } 498 | 499 | num_sent_emails = num_sent_emails + 1 500 | email_recipients = append(email_recipients, email) 501 | 502 | // mark both active and inactive connections as notified 503 | for _, proxy_ref := range proxy_ref_list { 504 | if proxy_ref.Active { 505 | // mark as notified 506 | var actual_ref = references.Proxies[proxy_ref.Name] 507 | actual_ref.Notified = true 508 | 509 | references.Proxies[proxy_ref.Name] = actual_ref 510 | } 511 | } 512 | 513 | for _, proxy_ref := range proxy_ref_list { 514 | if !proxy_ref.Active { 515 | // mark as notified 516 | var actual_ref = references.Proxies[proxy_ref.Name] 517 | actual_ref.Notified = true 518 | 519 | references.Proxies[proxy_ref.Name] = actual_ref 520 | 521 | } 522 | } 523 | } 524 | } 525 | 526 | fmt.Printf("[linknotifier]: Notification email sent to %d recipient(s): %s\n", num_sent_emails, strings.Join(email_recipients[:],", ")) 527 | 528 | // save updated links 529 | saveProxyLinksJSON() 530 | } 531 | 532 | 533 | mutex.Unlock() 534 | last_notified = time.Now() 535 | } 536 | } 537 | time.Sleep(time.Duration(FRPS_LINK_NOTIFIER_SLEEP_CHECK_SEC) * time.Second) 538 | } 539 | 540 | } 541 | 542 | func check_connection(proxy_ref ProxyInfo, FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC int) bool { 543 | var ok bool = false 544 | if proxy_ref.ProxyType == "tcp" || proxy_ref.ProxyType == "udp" { 545 | // check using direct connection 546 | conn, err := net.DialTimeout(proxy_ref.ProxyType, proxy_ref.Url, time.Duration(FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC)*time.Second) 547 | if err == nil && conn != nil { 548 | defer conn.Close() 549 | 550 | // connection is valid so we retain it 551 | ok = true 552 | } 553 | 554 | } else if proxy_ref.ProxyType == "http" || proxy_ref.ProxyType == "https" { 555 | 556 | client := http.Client{Timeout: time.Duration(FRPS_LINK_NOTIFIER_CONNECTION_CHECK_TIMEOUT_SEC) * time.Second} 557 | // check using HTTP request 558 | _, err := client.Get(proxy_ref.Url) 559 | if err == nil { 560 | // connection is valid so we retain it 561 | ok = true 562 | } 563 | } 564 | 565 | return ok 566 | } 567 | 568 | 569 | func main() { 570 | file, err := ioutil.ReadFile("links.json") 571 | 572 | if err == nil { 573 | err = json.Unmarshal([]byte(file), &references) 574 | } 575 | 576 | go notifier_main() 577 | 578 | http.HandleFunc("/", handler) 579 | http.ListenAndServe(fmt.Sprintf(":%d", getEnvInt("PLUGIN_PORT", 9003)), nil) 580 | } 581 | 582 | -------------------------------------------------------------------------------- /plugins/portmanager/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker-frps/plugins/portmanager 2 | 3 | go 1.12 4 | 5 | -------------------------------------------------------------------------------- /plugins/portmanager/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "bufio" 10 | "regexp" 11 | "strconv" 12 | ) 13 | 14 | type Request struct { 15 | Version string `json:"version"` 16 | Op string `json:"op"` 17 | Content map[string]interface{} `json:"content"` 18 | } 19 | 20 | type Response struct { 21 | Reject bool `json:"reject"` 22 | RejectReason string `json:"reject_reason"` 23 | Unchange bool `json:"unchange"` 24 | Content map[string]interface{} `json:"content"` 25 | } 26 | 27 | var mutex sync.RWMutex 28 | var ports = make(map[string]int) 29 | 30 | func getEnv(name string, def string) string { 31 | val := os.Getenv(name) 32 | 33 | if val == "" { 34 | return def 35 | } 36 | 37 | return val 38 | 39 | } 40 | 41 | func getEnvInt(name string, def int) int { 42 | val := os.Getenv(name) 43 | 44 | if val == "" { 45 | return def 46 | } 47 | 48 | ival, _ := strconv.ParseInt(val, 10, 32) 49 | 50 | return int(ival) 51 | } 52 | 53 | func check(e error) { 54 | if e != nil { 55 | panic(e) 56 | } 57 | } 58 | 59 | var portMin int = getEnvInt("PLUGIN_PORT_MIN", 30000) 60 | var portMax int = getEnvInt("PLUGIN_PORT_MAX", 30900) 61 | 62 | func savePortMapping() { 63 | 64 | f, err := os.Create("ports.map") 65 | check(err) 66 | 67 | for k, v := range ports { 68 | _, err := f.WriteString(fmt.Sprintf("%v %v\n", k, v)) 69 | check(err) 70 | } 71 | 72 | defer f.Close() 73 | } 74 | 75 | func handler(w http.ResponseWriter, r *http.Request) { 76 | 77 | switch r.Method { 78 | case "POST": 79 | d := json.NewDecoder(r.Body) 80 | r := &Request{} 81 | o := &Response{} 82 | err := d.Decode(r) 83 | if err != nil { 84 | http.Error(w, err.Error(), http.StatusInternalServerError) 85 | } 86 | 87 | if r.Op != "NewProxy" { 88 | w.WriteHeader(http.StatusMethodNotAllowed) 89 | fmt.Fprintf(w, "Not allowed.") 90 | return 91 | } 92 | 93 | if r.Content["proxy_type"] == "tcp" || r.Content["proxy_type"] == "udp" { 94 | 95 | var key = fmt.Sprintf("%v:%v", r.Content["proxy_name"], r.Content["proxy_type"]) 96 | var port int = int(r.Content["remote_port"].(float64)) 97 | 98 | // Allocate or retrieve port 99 | if port == 0 { 100 | 101 | mutex.Lock() 102 | 103 | port, ok := ports[key] 104 | 105 | if !ok { 106 | 107 | var allocated = make(map[int]bool) 108 | 109 | for _, v := range ports { 110 | allocated[v] = true 111 | } 112 | 113 | for i := portMin; i <= portMax; i++ { 114 | 115 | if !allocated[i] { 116 | port = i 117 | break 118 | } 119 | 120 | } 121 | 122 | if port == 0 { 123 | fmt.Printf("[portmanager - %s] WARNING: Unable to allocate port, all available ports already taken.\n", key) 124 | 125 | o.Reject = true 126 | o.RejectReason = "All available ports already taken" 127 | } else { 128 | fmt.Printf("[portmanager - %s] New client found, allocating new port: '%d'.\n", key, port) 129 | 130 | ports[key] = port 131 | savePortMapping() 132 | 133 | o.Reject = false 134 | o.Unchange = false 135 | o.Content = r.Content 136 | o.Content["remote_port"] = port 137 | 138 | } 139 | 140 | } else { 141 | fmt.Printf("[portmanager - %s] Known client ... using port %d.\n", key, port) 142 | 143 | o.Reject = false 144 | o.Unchange = false 145 | o.Content = r.Content 146 | o.Content["remote_port"] = port 147 | 148 | } 149 | 150 | mutex.Unlock() 151 | 152 | } else { 153 | // Verify that port is not taken 154 | 155 | mutex.Lock() 156 | 157 | var found bool = false 158 | 159 | fmt.Printf("[portmanager - %s] New client ... allocating requested port '%d'.\n", key, port) 160 | 161 | for k, v := range ports { 162 | if v == port { 163 | if k == key { 164 | o.Reject = false 165 | o.Unchange = true 166 | } else { 167 | o.Reject = true 168 | o.RejectReason = "Port already taken by another proxy" 169 | fmt.Printf("[portmanager - %s] WARNING: Cannot allocate, port already taken by another client!.\n", key, port) 170 | } 171 | found = true 172 | } 173 | } 174 | 175 | if !found { 176 | if port >= portMin && port <= portMax { 177 | ports[key] = port 178 | o.Reject = false 179 | o.Unchange = true 180 | 181 | savePortMapping() 182 | 183 | } else { 184 | o.Reject = true 185 | o.RejectReason = "Illegal port number" 186 | fmt.Printf("[portmanager - %s] WARNING: Illegal port number requested!.\n", key, port) 187 | } 188 | } 189 | 190 | mutex.Unlock() 191 | 192 | } 193 | 194 | } else { 195 | o.Reject = false 196 | o.Unchange = true 197 | } 198 | 199 | js, err := json.Marshal(o) 200 | if err != nil { 201 | http.Error(w, err.Error(), http.StatusInternalServerError) 202 | return 203 | } 204 | w.Header().Set("Content-Type", "application/json") 205 | w.Write(js) 206 | 207 | default: 208 | w.WriteHeader(http.StatusMethodNotAllowed) 209 | fmt.Fprintf(w, "I can't do that.") 210 | } 211 | } 212 | 213 | func main() { 214 | 215 | mutex.Lock() 216 | 217 | f, err := os.Open("ports.map") 218 | 219 | if !os.IsNotExist(err) { 220 | 221 | fmt.Printf("[portmanager]: Reading cached port mapping in 'ports.map'\n") 222 | var lineParser = regexp.MustCompile(`^(\S+) ([0-9]+)$`) 223 | 224 | s := bufio.NewScanner(f) 225 | for s.Scan() { 226 | line := s.Text() 227 | 228 | matches := lineParser.FindSubmatch([]byte(line)) 229 | 230 | if len(matches) == 3 { 231 | port64, _ := strconv.ParseInt(string(matches[2]), 10, 32) 232 | port := int(port64) 233 | 234 | if port >= portMin && port <= portMax { 235 | fmt.Printf("[portmanager]: Found port %d for %s\n", port, string(matches[1])) 236 | ports[string(matches[1])] = int(port) 237 | } else { 238 | fmt.Printf("[portmanager]: Found port %d for %s BUT DOES NOT MATCH LIMITS %d <= port <= %d.\n", port, string(matches[1]), portMin, portMax) 239 | } 240 | 241 | } else { 242 | fmt.Printf("[portmanager]: Line does ot contain three parts: '%s'.\n", line) 243 | } 244 | 245 | } 246 | 247 | fmt.Printf("[portmanager]: Done - total cached ports found: %d.\n", len(ports)) 248 | f.Close() 249 | } else { 250 | fmt.Printf("[portmanager]: No cache for port mapping 'ports.map' file found\n") 251 | } 252 | 253 | mutex.Unlock() 254 | 255 | http.HandleFunc("/", handler) 256 | http.ListenAndServe(fmt.Sprintf(":%d", getEnvInt("PLUGIN_PORT", 9001)), nil) 257 | } 258 | 259 | -------------------------------------------------------------------------------- /start_runit: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | #env | sed -e 's/=/="/' | sed -e 's/$/"/' > /etc/envvars 4 | for K in $(env | cut -d= -f1) 5 | do 6 | VAL=$(eval echo \$$K) 7 | echo "${K}=\"${VAL}\"" >> /etc/envvars 8 | done 9 | 10 | echo "Starting pre-service scripts in /etc/runit_init.d" 11 | for script in /etc/runit_init.d/* 12 | do 13 | if [ -x "$script" ]; then 14 | echo >&2 "*** Running: $script" 15 | $script 16 | retval=$? 17 | if [ $retval != 0 ]; 18 | then 19 | echo >&2 "*** Failed with return value: $?" 20 | exit $retval 21 | fi 22 | fi 23 | done 24 | if [ $# -eq 0 ]; then 25 | exec /usr/sbin/runsvdir -P /etc/service 26 | fi 27 | /usr/sbin/runsvdir -P /etc/service & 28 | 29 | [ "$1" == '--' ] && shift 30 | exec $@ 31 | --------------------------------------------------------------------------------