mohammed90 / caddy-ngrok-listener

Caddy listener_wrapper to automatically listen on an ngrok tunnel
Apache License 2.0
20 stars 2 forks source link

Multi-Listener? (Wrap Caddy Listener vs. Ignore Caddy Listener) #19

Open mohammed90 opened 1 year ago

mohammed90 commented 1 year ago

[@jtszalay copying and moving to an issue for easier tracking 🙂]

I kept trying to figure out how to make it listen on the ngrok listener AND the default caddy listener.

Ignoring Caddy's listener just felt natural for the purpose of the module 😂

I've since come across this project this week: https://github.com/daniel-garcia/multilistener/blob/master/listener.go Thoughts on making this a multilistener so that local address and port still work?

Intriguing! But I'm not sure whether users will naturally expect both listeners to work, because it's a "wrapper", or only ngrok's listener because it's ngrok. Do you think it's possible to have a toggle? You don't have to go the toggle route, I'm genuinely curious which way is better.

I don't personally use ngrok, even though I drafted this module, so I can't have a solid opinion on it. What do you think users expect?

Originally posted by @mohammed90 in https://github.com/mohammed90/caddy-ngrok-listener/pull/4#issuecomment-1472777212

jtszalay commented 1 year ago

Previous to having this wrapper I stood up an ngrok agent in a docker container and pointed it at my caddy container. I still exposed my caddy port locally and would access the port on my local network when I didn't want to go out to the internet. That would be my usecase.

But after further thought I can just do the following couldn't I?

{
    servers :80 {
        listener_wrappers {
            ngrok {
                auth_token $NGROK_AUTH_TOKEN
                tunnel http {
                }
            }
        }
    }
        servers :8080 {
    }
}
:80, :8080 {
    root * /path/to/site/root
    file_server
}

If that's the case then caddy already provides the solution to this and we likely don't need to do this.

mohammed90 commented 1 year ago

That works too. However, I've been doing some thinking. Given the module is within listener_wrapper, it might be a bit confusing for the user to have a Caddy network address that is never used. The multilistener Go module makes sense here.

We can extend this repo by adding another module that makes use of Caddy's custom network registration to obtain only an ngrok listener for the given address. The address in this case an indicator of which ngrok tunnel to use. The code for that is as so:

```go package ngroklistener import ( "context" "encoding/json" "fmt" "time" "github.com/caddyserver/caddy/v2" "go.uber.org/zap" "golang.ngrok.com/ngrok" ngrokZap "golang.ngrok.com/ngrok/log/zap" ) func init() { caddy.RegisterNetwork("ngrok", func(ctx context.Context, _, addr string, _ net.ListenConfig) (any, error) { app, err := caddy.ActiveContext().App("ngrok") if err != nil { return nil, err } var ngrokApp *App var ok bool ngrokApp, ok = app.(*App) if !ok || app == nil { panic("TODO") } tun, ok := ngrokApp.tunnels[addr] if !ok { panic("TODO") } return ngrok.Listen( ctx, tun.NgrokTunnel(), ngrokApp.opts..., ) }) caddy.RegisterModule(new(App)) } type App struct { // The user's ngrok authentication token AuthToken string `json:"authtoken,omitempty"` // a map of tunnels as identified by the given name that is the key TunnelsRaw map[string]json.RawMessage `json:"tunnels,omitempty" caddy:"namespace=caddy.listeners.ngrok.tunnels inline_key=type"` // Opaque, machine-readable metadata string for this session. // Metadata is made available to you in the ngrok dashboard and the // Agents API resource. It is a useful way to allow you to uniquely identify // sessions. We suggest encoding the value in a structured format like JSON. Metadata string `json:"metadata,omitempty"` // Region configures the session to connect to a specific ngrok region. // If unspecified, ngrok will connect to the fastest region, which is usually what you want. // The [full list of ngrok regions] can be found in the ngrok documentation. Region string `json:"region,omitempty"` // Server configures the network address to dial to connect to the ngrok // service. Use this option only if you are connecting to a custom agent // ingress. // // See the [server_addr parameter in the ngrok docs] for additional details. Server string `json:"server,omitempty"` // HeartbeatTolerance configures the duration to wait for a response to a heartbeat // before assuming the session connection is dead and attempting to reconnect. // // See the [heartbeat_tolerance parameter in the ngrok docs] for additional details. HeartbeatTolerance caddy.Duration `json:"heartbeat_tolerance,omitempty"` // HeartbeatInterval configures how often the session will send heartbeat // messages to the ngrok service to check session liveness. // // See the [heartbeat_interval parameter in the ngrok docs] for additional details. HeartbeatInterval caddy.Duration `json:"heartbeat_interval,omitempty"` opts []ngrok.ConnectOption tunnels map[string]Tunnel ctx context.Context l *zap.Logger } // CaddyModule implements caddy.Module func (*App) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "ngrok", New: func() caddy.Module { return new(App) }, } } // Provisions the ngrok listener wrapper func (n *App) Provision(ctx caddy.Context) error { n.ctx = ctx n.l = ctx.Logger() if n.TunnelsRaw == nil { n.TunnelsRaw = map[string]json.RawMessage{ "default": json.RawMessage(`{"type": "tcp"}`), } } tmod, err := ctx.LoadModule(n, "TunnelsRaw") if err != nil { return fmt.Errorf("loading ngrok tunnel module: %v", err) } for k, v := range tmod.(map[string]Tunnel) { n.tunnels[k] = v } if err = n.doReplace(); err != nil { return fmt.Errorf("loading doing replacements: %v", err) } if err = n.provisionOpts(); err != nil { return fmt.Errorf("provisioning ngrok opts: %v", err) } return nil } func (n *App) provisionOpts() error { n.opts = append(n.opts, ngrok.WithLogger(ngrokZap.NewLogger(n.l))) if n.AuthToken == "" { n.opts = append(n.opts, ngrok.WithAuthtokenFromEnv()) } else { n.opts = append(n.opts, ngrok.WithAuthtoken(n.AuthToken)) } if n.Metadata != "" { n.opts = append(n.opts, ngrok.WithMetadata(n.Metadata)) } if n.Region != "" { n.opts = append(n.opts, ngrok.WithRegion(n.Region)) } if n.Server != "" { n.opts = append(n.opts, ngrok.WithServer(n.Server)) } n.opts = append(n.opts, ngrok.WithHeartbeatInterval(time.Duration(n.HeartbeatInterval))) n.opts = append(n.opts, ngrok.WithHeartbeatTolerance(time.Duration(n.HeartbeatTolerance))) return nil } func (n *App) doReplace() error { repl := caddy.NewReplacer() replaceableFields := []*string{ &n.AuthToken, &n.Metadata, &n.Region, &n.Server, } for _, field := range replaceableFields { actual := repl.ReplaceKnown(*field, "") *field = actual } return nil } // Start implements caddy.App func (*App) Start() error { return nil } // Stop implements caddy.App func (*App) Stop() error { return nil } var ( _ caddy.App = (*App)(nil) _ caddy.Module = (*App)(nil) ) ```

An example configuration for such is:

```json { "apps": { "ngrok": { "authtoken": "{env.NGROK_AUTH_TOKEN}", "tunnels": { "my-http-tunnel": { "type": "http" }, "labeled-tunnel-1": { "type": "label" }, "raw-tcp": { "type": "tcp" } } }, "http": { "servers": { "srv0": { "listen": [ "ngrok/labeled-tunnel-1" ], "routes": [ { "handle": [ { "handler": "subroute", "routes": [ { "handle": [ { "handler": "file_server", "hide": [ "./Caddyfile-2" ] } ] } ] } ], "terminal": true } ] }, "srv1": { "listen": [ "ngrok/raw-tcp" ], "routes": [ { "handle": [ { "handler": "subroute", "routes": [ { "handle": [ { "handler": "static_response", "body": "Hello!" } ] } ] } ], "terminal": true } ] } } } } } ```

What do you think?