diff --git a/_examples/README.md b/_examples/README.md
index 06d1d632..72c7ef39 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -78,6 +78,7 @@ Structuring depends on your own needs. We can't tell you how to design your own
### HTTP Listening
- [Common, with address](http-listening/listen-addr/main.go)
+ * [public domain address](http-listening/listen-addr-public/main.go) **NEW**
* [omit server errors](http-listening/listen-addr/omit-server-errors/main.go)
- [UNIX socket file](http-listening/listen-unix/main.go)
- [TLS](http-listening/listen-tls/main.go)
diff --git a/_examples/http-listening/listen-addr-public/README.md b/_examples/http-listening/listen-addr-public/README.md
new file mode 100644
index 00000000..c29e185d
--- /dev/null
+++ b/_examples/http-listening/listen-addr-public/README.md
@@ -0,0 +1 @@
+![tunneling_screenshot.png](tunneling_screenshot.png)
diff --git a/_examples/http-listening/listen-addr-public/main.go b/_examples/http-listening/listen-addr-public/main.go
new file mode 100644
index 00000000..d0be9d44
--- /dev/null
+++ b/_examples/http-listening/listen-addr-public/main.go
@@ -0,0 +1,36 @@
+package main
+
+import "github.com/kataras/iris"
+
+func main() {
+ app := iris.New()
+
+ app.Get("/", func(ctx iris.Context) {
+ ctx.HTML("
Hello World!
")
+
+ // Will print the ngrok public domain
+ // that your app is using to be served online.
+ ctx.Writef("From: %s",
+ ctx.Application().ConfigurationReadOnly().GetVHost())
+ })
+
+ app.Run(iris.Addr(":8080"), iris.WithTunneling)
+
+ /* The full configuration can be set as:
+ app.Run(iris.Addr(":8080"), iris.WithConfiguration(
+ iris.Configuration{
+ Tunneling: iris.TunnelingConfiguration{
+ AuthToken: "my-ngrok-auth-client-token",
+ Bin: "/bin/path/for/ngrok",
+ Region: "eu",
+ WebInterface: "127.0.0.1:4040",
+ Tunnels: []iris.Tunnel{
+ {
+ Name: "MyApp",
+ Addr: ":8080",
+ },
+ },
+ },
+ }))
+ */
+}
diff --git a/_examples/http-listening/listen-addr-public/tunneling_screenshot.png b/_examples/http-listening/listen-addr-public/tunneling_screenshot.png
new file mode 100644
index 00000000..df44adf9
Binary files /dev/null and b/_examples/http-listening/listen-addr-public/tunneling_screenshot.png differ
diff --git a/configuration.go b/configuration.go
index 7ea82425..8901827c 100644
--- a/configuration.go
+++ b/configuration.go
@@ -1,8 +1,13 @@
package iris
import (
+ "bytes"
+ "encoding/json"
+ "fmt"
"io/ioutil"
+ "net/http"
"os"
+ "os/exec"
"os/user"
"path/filepath"
"runtime"
@@ -346,7 +351,7 @@ func WithoutRemoteAddrHeader(headerName string) Configurator {
// WithOtherValue adds a value based on a key to the Other setting.
//
-// See `Configuration`.
+// See `Configuration.Other`.
func WithOtherValue(key string, val interface{}) Configurator {
return func(app *Application) {
if app.config.Other == nil {
@@ -356,61 +361,216 @@ func WithOtherValue(key string, val interface{}) Configurator {
}
}
-// WithTunnel is the `iris.Configurator` for the `iris.Configuration.Tunnel` field.
-// It requires the "name" which is used to create on server ran and terminate on server shutdown
-// the ngrok http(s) tunnel for an Iris app.
-// Its second variadic input argument can accept one or more functions
-// that accept a pointer to TunnelConfiguration value which can be used
-// for further customization of the tunnel configuration one.
-// Alternatively use the `iris.WithConfiguration(iris.Configuration{Tunnel: iris.TunnelConfiguration{ ...}}}`.
-func WithTunnel(name string, tunnelConfigurator ...func(*TunnelConfiguration)) Configurator {
- conf := &TunnelConfiguration{Name: name}
-
- for _, tc := range tunnelConfigurator {
- if tc != nil {
- tc(conf)
- }
+// WithTunneling is the `iris.Configurator` for the `iris.Configuration.Tunneling` field.
+// It's used to enable http tunneling for an Iris Application, per registered host
+//
+// Alternatively use the `iris.WithConfiguration(iris.Configuration{Tunneling: iris.TunnelingConfiguration{ ...}}}`.
+func WithTunneling(app *Application) {
+ conf := TunnelingConfiguration{
+ Tunnels: []Tunnel{{}}, // create empty tunnel, its addr and name are set right before host serve.
}
- return func(app *Application) {
- app.config.Tunnel = *conf
- // TODO: do the work here if the vhost is set (when this configurator is set through app.Run)
- // or find a way to do it when `app.Configure` is used instead, probably it will go inside app.Run though.
- }
+ app.config.Tunneling = conf
}
-// TunnelConfiguration contains configuration
-// for the optional tunneling through ngrok feature.
-type TunnelConfiguration struct {
+// Tunnel is the Tunnels field of the TunnelingConfiguration structure.
+type Tunnel struct {
// Name is the only one required field,
// it is used to create and close tunnels, e.g. "MyApp".
// If this field is not empty then ngrok tunnels will be created
// when the iris app is up and running.
Name string `json:"name" yaml:"Name" toml:"Name"`
- // Usernamem and Password fields are optionally and are used
- // to authenticate the ngrok tunnel access.
- Username string `json:"username,omitempty" yaml:"Username" toml:"Username"`
- Password string `json:"password,omitemmpty" yaml:"Password" toml:"Password"`
-
- // Bin is the system binary path of the ngrok executable file.
- // If it's empty then the framework will try to find it through system env variables.
- Bin string `json:"bin,omitempty" yaml:"Bin" toml:"Bin"`
// Addr is basically optionally as it will be set through
// Iris built-in Runners, however, if `iris.Raw` is used
// then this field should be set of form 'hostname:port'
// because framework cannot be aware
// of the address you used to run the server on this custom runner.
Addr string `json:"addr,omitempty" yaml:"Addr" toml:"Addr"`
+}
+
+// TunnelingConfiguration contains configuration
+// for the optional tunneling through ngrok feature.
+// Note that the ngrok should be already installed at the host machine.
+type TunnelingConfiguration struct {
+ // AuthToken field is optionally and can be used
+ // to authenticate the ngrok access.
+ // ngrok authtoken
+ AuthToken string `json:"authToken,omitempty" yaml:"AuthToken" toml:"AuthToken"`
+
+ // No...
+ // Config is optionally and can be used
+ // to load ngrok configuration from file system path.
+ //
+ // If you don't specify a location for a configuration file,
+ // ngrok tries to read one from the default location $HOME/.ngrok2/ngrok.yml.
+ // The configuration file is optional; no error is emitted if that path does not exist.
+ // Config string `json:"config,omitempty" yaml:"Config" toml:"Config"`
+
+ // Bin is the system binary path of the ngrok executable file.
+ // If it's empty then the framework will try to find it through system env variables.
+ Bin string `json:"bin,omitempty" yaml:"Bin" toml:"Bin"`
+
// WebUIAddr is the web interface address of an already-running ngrok instance.
// Iris will try to fetch the default web interface address(http://127.0.0.1:4040)
// to determinate if a ngrok instance is running before try to start it manually.
// However if a custom web interface address is used,
// this field must be set e.g. http://127.0.0.1:5050.
- WebInterface string `json:"webInterface" yaml:"WebInterface" toml:"WebInterface"`
+ WebInterface string `json:"webInterface,omitempty" yaml:"WebInterface" toml:"WebInterface"`
+
+ // Region is optionally, can be used to set the region which defaults to "us".
+ // Available values are:
+ // "us" for United States
+ // "eu" for Europe
+ // "ap" for Asia/Pacific
+ // "au" for Australia
+ // "sa" for South America
+ // "jp" forJapan
+ // "in" for India
+ Region string `json:"region,omitempty" yaml:"Region" toml:"Region"`
+
+ // Tunnels the collection of the tunnels.
+ // One tunnel per Iris Host per Application, usually you only need one.
+ Tunnels []Tunnel `json:"tunnels" yaml:"Tunnels" toml:"Tunnels"`
}
-func (tc *TunnelConfiguration) isEnabled() bool {
- return tc != nil && tc.Name != ""
+func (tc *TunnelingConfiguration) isEnabled() bool {
+ return tc != nil && len(tc.Tunnels) > 0
+}
+
+func (tc *TunnelingConfiguration) isNgrokRunning() bool {
+ _, err := http.Get(tc.WebInterface)
+ return err == nil
+}
+
+// https://ngrok.com/docs
+type ngrokTunnel struct {
+ Name string `json:"name"`
+ Addr string `json:"addr"`
+ Proto string `json:"proto"`
+ Auth string `json:"auth"`
+ BindTLS bool `json:"bind_tls"`
+}
+
+func (tc TunnelingConfiguration) startTunnel(t Tunnel, publicAddr *string) error {
+ tunnelAPIRequest := ngrokTunnel{
+ Name: t.Name,
+ Addr: t.Addr,
+ Proto: "http",
+ BindTLS: true,
+ }
+
+ if !tc.isNgrokRunning() {
+ ngrokBin := "ngrok" // environment binary.
+ if tc.Bin != "" {
+ ngrokBin = tc.Bin
+ }
+
+ if tc.AuthToken != "" {
+ cmd := exec.Command(ngrokBin, "authtoken", tc.AuthToken)
+ err := cmd.Run()
+ if err != nil {
+ return err
+ }
+ }
+
+ // start -none, start without tunnels.
+ // and finally the -log stdout logs to the stdout otherwise the pipe will never be able to read from, spent a lot of time on this lol.
+ cmd := exec.Command(ngrokBin, "start", "-none", "-log", "stdout")
+
+ // if tc.Config != "" {
+ // cmd.Args = append(cmd.Args, []string{"-config", tc.Config}...)
+ // }
+ if tc.Region != "" {
+ cmd.Args = append(cmd.Args, []string{"-region", tc.Region}...)
+ }
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
+ }
+
+ if err = cmd.Start(); err != nil {
+ return err
+ }
+
+ p := make([]byte, 256)
+ okText := []byte("client session established")
+ for {
+ n, err := stdout.Read(p)
+ if err != nil {
+ return err
+ }
+
+ // we need this one:
+ // msg="client session established"
+ // note that this will block if something terrible happens
+ // but ngrok's errors are strong so the error is easy to be resolved without any logs.
+ if bytes.Contains(p[:n], okText) {
+ break
+ }
+ }
+ }
+
+ return tc.createTunnel(tunnelAPIRequest, publicAddr)
+}
+
+func (tc TunnelingConfiguration) stopTunnel(t Tunnel) error {
+ url := fmt.Sprintf("%s/api/tunnels/%s", tc.WebInterface, t.Name)
+ req, err := http.NewRequest(http.MethodDelete, url, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != StatusNoContent {
+ return fmt.Errorf("stop return an unexpected status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+func (tc TunnelingConfiguration) createTunnel(tunnelAPIRequest ngrokTunnel, publicAddr *string) error {
+ url := fmt.Sprintf("%s/api/tunnels", tc.WebInterface)
+ requestData, err := json.Marshal(tunnelAPIRequest)
+ if err != nil {
+ return err
+ }
+
+ resp, err := http.Post(url, context.ContentJSONHeaderValue, bytes.NewBuffer(requestData))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ type publicAddrOrErrResp struct {
+ PublicAddr string `json:"public_url"`
+ Details struct {
+ ErrorText string `json:"err"` // when can't bind more addresses, status code was successful.
+ } `json:"details"`
+ ErrMsg string `json:"msg"` // when ngrok is not yet ready, status code was unsuccessful.
+ }
+
+ var apiResponse publicAddrOrErrResp
+
+ err = json.NewDecoder(resp.Body).Decode(&apiResponse)
+ if err != nil {
+ return err
+ }
+
+ if errText := apiResponse.ErrMsg; errText != "" {
+ return errors.New(errText)
+ }
+
+ if errText := apiResponse.Details.ErrorText; errText != "" {
+ return errors.New(errText)
+ }
+
+ *publicAddr = apiResponse.PublicAddr
+ return nil
}
// Configuration the whole configuration for an iris instance
@@ -421,11 +581,9 @@ type Configuration struct {
// It can be retrieved by the context if needed (i.e router for subdomains)
vhost string
- // TunnelConfiguration can be optionally set
- // to enable ngrok http(s) tunneling for this Iris app instance.
- // If iris logger's level is set to "debug" then it will print out
- // ngrok tunneling info messages too.
- Tunnel TunnelConfiguration `json:"tunnel,omitempty" yaml:"Tunnel" toml"Tunnel"`
+ // Tunneling can be optionally set to enable ngrok http(s) tunneling for this Iris app instance.
+ // See the `WithTunneling` Configurator too.
+ Tunneling TunnelingConfiguration `json:"tunneling,omitempty" yaml:"Tunneling" toml"Tunneling"`
// IgnoreServerErrors will cause to ignore the matched "errors"
// from the main application's `Run` function.
@@ -740,8 +898,8 @@ func WithConfiguration(c Configuration) Configurator {
return func(app *Application) {
main := app.config
- if c.Tunnel.isEnabled() {
- main.Tunnel = c.Tunnel
+ if c.Tunneling.isEnabled() {
+ main.Tunneling = c.Tunneling
}
if v := c.IgnoreServerErrors; len(v) > 0 {
diff --git a/go19.go b/go19.go
index 5be3dafc..79cb6b76 100644
--- a/go19.go
+++ b/go19.go
@@ -62,7 +62,10 @@ type (
// Look the `core/router#APIBuilder` for its implementation.
//
// A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used.
- Party = router.Party
+ Party = router.Party
+ // DirOptions contains the optional settings that
+ // `FileServer` and `Party#HandleDir` can use to serve files and assets.
+ // A shortcut for the `router.DirOptions`, useful when `FileServer` or `HandleDir` is being used.
DirOptions = router.DirOptions
// ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves.
// Usage:
diff --git a/iris.go b/iris.go
index 2a445262..092968e2 100644
--- a/iris.go
+++ b/iris.go
@@ -2,11 +2,14 @@ package iris
import (
// std packages
+
stdContext "context"
+ "fmt"
"io"
"log"
"net"
"net/http"
+ "strings"
"sync"
"time"
@@ -365,7 +368,14 @@ var (
//
// A shortcut for the `context#NewConditionalHandler`.
NewConditionalHandler = context.NewConditionalHandler
-
+ // FileServer returns a Handler which serves files from a specific system, phyisical, directory
+ // or an embedded one.
+ // The first parameter is the directory, relative to the executable program.
+ // The second optional parameter is any optional settings that the caller can use.
+ //
+ // See `Party#HandleDir` too.
+ // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server
+ // A shortcut for the `router.FileServer`.
FileServer = router.FileServer
// StripPrefix returns a handler that serves HTTP requests
// by removing the given prefix from the request URL's Path
@@ -586,6 +596,14 @@ var RegisterOnInterrupt = host.RegisterOnInterrupt
// Shutdown gracefully terminates all the application's server hosts.
// Returns an error on the first failure, otherwise nil.
func (app *Application) Shutdown(ctx stdContext.Context) error {
+ for _, t := range app.config.Tunneling.Tunnels {
+ if t.Name == "" {
+ continue
+ }
+
+ app.config.Tunneling.stopTunnel(t)
+ }
+
for i, su := range app.Hosts {
app.logger.Debugf("Host[%d]: Shutdown now", i)
if err := su.Shutdown(ctx); err != nil {
@@ -822,6 +840,8 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error {
}
app.Configure(withOrWithout...)
+ app.tryStartTunneling()
+
app.logger.Debugf("Application: running using %d host(s)", len(app.Hosts)+1)
// this will block until an error(unless supervisor's DeferFlow called from a Task).
@@ -832,3 +852,55 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error {
return err
}
+
+// https://ngrok.com/docs
+func (app *Application) tryStartTunneling() {
+ if !app.config.Tunneling.isEnabled() {
+ return
+ }
+
+ hostIndex := 0
+
+ app.ConfigureHost(func(su *host.Supervisor) {
+ su.RegisterOnServe(func(h host.TaskHost) {
+ tc := app.config.Tunneling
+ if tc.WebInterface == "" {
+ tc.WebInterface = "http://127.0.0.1:4040"
+ }
+
+ if len(tc.Tunnels) > hostIndex {
+ t := tc.Tunnels[hostIndex]
+ if t.Name == "" {
+ t.Name = fmt.Sprintf("iris-app-%d-%s", hostIndex+1, time.Now().Format(app.config.TimeFormat))
+ }
+
+ if t.Addr == "" {
+ t.Addr = su.Server.Addr
+ }
+
+ var publicAddr string
+ err := tc.startTunnel(t, &publicAddr)
+ if err != nil {
+ app.Logger().Errorf("Host: tunneling error: %v", err)
+ return
+ }
+
+ // to make subdomains resolution still based on this new remote, public addresses.
+ app.config.vhost = publicAddr[strings.Index(publicAddr, "://")+3:]
+
+ // app.logger.Debugf("Host: new virtual host is %s", app.config.vhost)
+ // app.Logger().Printer.Output.Write([]byte("))
+
+ // directLog := []byte(fmt.Sprintf("| Public Address: %s |\n", publicAddr))
+ // box := bytes.Repeat([]byte("="), len(directLog))
+ // directLog = append(append(box, []byte("\n")...), append(directLog, append(box, []byte("\n")...)...)...)
+ directLog := []byte(fmt.Sprintf("⬝ Public Address: %s\n", publicAddr))
+ app.Logger().Printer.Output.Write(directLog)
+
+ }
+
+ hostIndex++
+ })
+ })
+
+}