cloudflare-tunnel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c80792cd4e7e48be6c97df5edf07c554d6285394607f70926e4e2a0d019572c7
4
+ data.tar.gz: b0bd9ffc636ec2029a83234d8172d5c8fba76ac20ba8b4e53b42e56c5df4b831
5
+ SHA512:
6
+ metadata.gz: d91a8ac77dd2452910e816080413197b61c4d4b1a6353aa7a75dc9f2e2270ed4f24c14e17163aa00b11b999555ea2df0a535de36891d9b3004bf52c0dfb46c40
7
+ data.tar.gz: 6150c30d7d44ab48f3ecfe78fc2e9d192ebd4e41da51b4c31f7c77622a2f5465849923753d110c39fdcca0bee337e6a6722f218c4431e3c6be97e0be8832ca8b
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ ## [0.1.0] - 2026-11-04
2
+
3
+ ### Added
4
+
5
+ - Rails configuration generator and explicit development tunnel tasks.
6
+ - Quiet offline retries, configurable cloudflared log levels and separate logs.
7
+ - Installation guidance without automatic executable installation.
8
+ - Connector readiness and signed local/public application diagnostics.
9
+ - Secure temporary tokens, per-app locking and bounded child shutdown.
10
+ - Local quality checks and tag-triggered GitHub Actions publishing.
11
+
12
+ ### Changed
13
+
14
+ - Release preparation runs from main, checks for duplicate tags, runs local
15
+ checks, commits version and changelog updates, and pushes the branch and tag.
16
+ - The first release can use the existing version and changelog without a version
17
+ bump or empty commit. Publishing runs in GitHub Actions after the tag is pushed.
18
+
19
+ ### Fixed
20
+
21
+ - Use a shared credentials action with an explicit RubyGems owner for trusted
22
+ publishing of both first and subsequent releases.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Paulo Fidalgo and Ethos Link
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # Cloudflare Tunnel
2
+
3
+ Run a remotely-managed Cloudflare tunnel alongside a Rails development server.
4
+ Offline startup and connection loss stay quiet and do not stop the server or
5
+ jobs. The gem supervises an existing `cloudflared` executable; it never installs,
6
+ downloads or updates it, creates a tunnel, or changes DNS.
7
+
8
+ Requires Ruby 4.0+, Rails 8.1 and a POSIX host (Linux or macOS).
9
+ MIT licensed; maintained by [Ethos Link](https://www.ethos-link.com).
10
+ This is an independent integration.
11
+
12
+ ## Rails setup
13
+
14
+ Add to the development group:
15
+
16
+ ```ruby
17
+ group :development do
18
+ gem "cloudflare-tunnel", "~> 0.1"
19
+ end
20
+ ```
21
+
22
+ ```sh
23
+ bundle install
24
+ bin/rails generate cloudflare_tunnel:install \
25
+ --hostname=devel.example.com --port=3000 --metrics-port=20241
26
+ ```
27
+
28
+ The generator creates `config/initializers/cloudflare_tunnel.rb` and adds
29
+ `tunnel: bin/rails cloudflare_tunnel:start` to `Procfile.dev`.
30
+
31
+ Install the executable yourself using [Cloudflare's instructions](https://developers.cloudflare.com/tunnel/downloads/).
32
+ When missing, the start task prints that link once and stays idle so Foreman
33
+ keeps other services running. The check task reports it and exits with failure.
34
+
35
+ In Cloudflare, configure a remotely-managed tunnel's published application
36
+ hostname to use `http://localhost:3000` (match your Rails port). Store its
37
+ connector token with `bin/rails credentials:edit`:
38
+
39
+ ```yaml
40
+ cloudflare:
41
+ tunnel:
42
+ token: YOUR_TUNNEL_TOKEN
43
+ ```
44
+
45
+ `TUNNEL_TOKEN` overrides credentials. Tokens use temporary files with mode `0600`,
46
+ stay out of command arguments and diagnostics, and are removed on normal exit,
47
+ INT and TERM. A forced OS kill cannot run cleanup; remove abandoned
48
+ `cloudflare-tunnel-*.token` files from your OS temporary directory if needed.
49
+
50
+ ```sh
51
+ bin/dev
52
+ # In a second terminal, after Rails and the tunnel are running:
53
+ bin/rails cloudflare_tunnel:check
54
+ ```
55
+
56
+ Adding the gem or booting Rails never starts a tunnel. Only the explicit start
57
+ task does. Tasks refuse to run outside development. The diagnostic middleware
58
+ is installed only in development. The generated initializer guards against the
59
+ gem being excluded from other Bundler groups.
60
+
61
+ ## Configuration
62
+
63
+ ```ruby
64
+ if Rails.env.development?
65
+ Cloudflare::Tunnel.configure do |config|
66
+ config.hostname = "devel.example.com"
67
+ config.origin = "http://localhost:#{ENV.fetch('PORT', '3000')}"
68
+ config.metrics_port = 20241
69
+ config.log_level = :error
70
+ config.log_path = "log/cloudflare-tunnel.log"
71
+ config.retry_interval = 30
72
+ end
73
+ end
74
+ ```
75
+
76
+ | Option | Default | Purpose |
77
+ | --- | --- | --- |
78
+ | `hostname` | required | Bare public hostname configured in Cloudflare |
79
+ | `origin` | `http://localhost:$PORT` (`3000` otherwise) | Expected Rails origin; change Cloudflare's origin setting separately |
80
+ | `metrics_port` | `20241` | Readiness/metrics port bound to `127.0.0.1`; choose a distinct port for every concurrent app |
81
+ | `log_level` | `:error` | Native cloudflared level: `:debug`, `:info`, `:warn`, `:error`, `:fatal` |
82
+ | `log_path` | `log/cloudflare-tunnel.log` | Append connector output here, relative to Rails root; `nil` discards it |
83
+ | `retry_interval` | `30` | Seconds before restarting an exited connector |
84
+ | `shutdown_timeout` | `5` | Seconds after TERM before killing a stuck child |
85
+ | `check_timeout` | `5` | HTTP connection/read/write timeouts, per diagnostic request |
86
+ | `executable` | `cloudflared` | Name on PATH or an explicit filesystem path |
87
+ | `enabled` | true | False when `TUNNEL` is `0`, `false`, `off` or `no`; configurable explicitly |
88
+ | `token` | lazy Rails credentials / `TUNNEL_TOKEN` | String or callable; keep real tokens out of initializers |
89
+
90
+ The connector handles its own reconnections. If it exits, the supervisor retries
91
+ after `retry_interval`, without recurring terminal messages. Disabled tunnels,
92
+ missing credentials and invalid configurations print one actionable message and
93
+ stay idle. Fix configuration and restart. The check fails for those states.
94
+
95
+ Use `TUNNEL=0 bin/dev` to disable the connector. Use your local Rails URL offline.
96
+ OAuth callbacks and provider requests still need internet. Turnstile behavior
97
+ remains application-owned.
98
+
99
+ The native log level filters cloudflared messages. Connector output stays out of
100
+ Rails logs and recurring Foreman output. Set `log_path = nil` to discard it.
101
+ Ignore `log/` and `tmp/` in Git; appended logs need normal local cleanup. Debug
102
+ logs may include request details; enable that level only while diagnosing.
103
+
104
+ A per-app file lock prevents two gem-managed connectors in the same working
105
+ directory. Stop older Docker or system-service connectors for that tunnel
106
+ yourself. Other working directories can still run replicas and receive traffic.
107
+
108
+ ## Diagnostics
109
+
110
+ `bin/rails cloudflare_tunnel:check` returns `0` only when all checks pass:
111
+
112
+ 1. Configuration, executable availability and token presence.
113
+ 2. The local Rails origin returns a fresh signed probe response.
114
+ 3. The connector's `/ready` endpoint reports an active Cloudflare connection.
115
+ 4. HTTPS on the public hostname returns the expected response from this app.
116
+
117
+ The development-only `/.well-known/cloudflare-tunnel?nonce=…` probe uses an HMAC
118
+ with a purpose-specific key derived from Rails' secret key base and returns
119
+ `Cache-Control: no-store`. It creates no records and exposes no credentials.
120
+ Fresh nonces prevent stale cached health pages passing the check. The signature
121
+ identifies the application's key, not a particular machine sharing that key.
122
+ The configured metrics port must belong to this connector.
123
+
124
+ Failures identify the layer and return `1`, including offline failures. The check
125
+ does not start a connector or change configuration. Redirects, Access login pages
126
+ and incorrect origins/app keys fail. Executable availability alone does not
127
+ validate the token; the running connector and end-to-end checks establish that.
128
+ Authentication errors remain in the separate connector log. No Cloudflare
129
+ management API token is required.
130
+
131
+ References: [run parameters](https://developers.cloudflare.com/tunnel/reference/run-parameters/)
132
+ and [readiness example](https://developers.cloudflare.com/tunnel/guides/kubernetes/).
133
+
134
+ ## Development
135
+
136
+ Run `bin/setup` to install development dependencies, then `bin/ci` to run
137
+ Minitest, Standard Ruby, the gem build and changelog generation. Tests control
138
+ connector executables and HTTP boundaries; they need no Cloudflare account or
139
+ internet once dependencies are installed.
140
+
141
+ Use `bundle exec rake -T` to list available tasks. See the
142
+ [changelog](CHANGELOG.md) for version history.
143
+
144
+ ## More from Ethos Link
145
+
146
+ Cloudflare Tunnel is maintained by [Ethos Link](https://www.ethos-link.com),
147
+ the software company behind Reviato and Nuntio. We build business products and
148
+ share open-source software.
149
+
150
+ - [Reviato — customer feedback and review analytics](https://www.reviato.com):
151
+ Collect private guest feedback and explore recurring themes in public reviews.
152
+ Hotels, restaurants and other local businesses can read the original customer
153
+ comments, track feedback and decide what needs attention.
154
+ - [Nuntio — social media scheduling and publishing](https://www.nuntiohq.com):
155
+ Prepare social posts ahead of time, adapt captions and media for each channel,
156
+ and bring drafts, team review, scheduling and publishing outcomes into one
157
+ workspace.
158
+
159
+ Explore [Ethos Link's products](https://www.ethos-link.com/products) and
160
+ [open-source projects](https://www.ethos-link.com/opensource).
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :cloudflare_tunnel do
4
+ desc "Run the optional development tunnel with quiet offline retries"
5
+ task start: :environment do
6
+ abort "cloudflare_tunnel:start is only available in development" unless Rails.env.development?
7
+ Cloudflare::Tunnel::Runner.new(Cloudflare::Tunnel.configuration).run
8
+ end
9
+
10
+ desc "Check configuration, connector, local origin and public app identity"
11
+ task check: :environment do
12
+ abort "cloudflare_tunnel:check is only available in development" unless Rails.env.development?
13
+ exit(Cloudflare::Tunnel::Check.new(Cloudflare::Tunnel.configuration).run ? 0 : 1)
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "securerandom"
5
+
6
+ module Cloudflare
7
+ module Tunnel
8
+ class Check
9
+ NETWORK_ERRORS = [SocketError, SystemCallError, Timeout::Error, IOError, OpenSSL::SSL::SSLError].freeze
10
+
11
+ def initialize(configuration, output: $stdout)
12
+ @configuration = configuration
13
+ @output = output
14
+ end
15
+
16
+ def run
17
+ issues = @configuration.errors
18
+ issues << "tunnel is disabled" unless @configuration.enabled
19
+ issues << "cloudflared not found. Install it yourself: #{INSTALL_URL}" unless @configuration.executable_path
20
+ issues << "token missing; set cloudflare.tunnel.token in Rails credentials" if @configuration.token_value.empty?
21
+ if issues.any?
22
+ issues.each { |issue| @output.puts("FAIL: #{issue}") }
23
+ return false
24
+ end
25
+
26
+ nonce = SecureRandom.hex(32)
27
+ expected = Probe.signature(@configuration.probe_secret.call, nonce)
28
+ local = verify("local origin", "#{@configuration.origin.to_s.chomp("/")}#{Probe::PATH}?nonce=#{nonce}", expected)
29
+ connector = verify("Cloudflare connection", "http://127.0.0.1:#{@configuration.metrics_port}/ready")
30
+ public_app = verify("public hostname", "https://#{@configuration.hostname}#{Probe::PATH}?nonce=#{nonce}", expected)
31
+ @output.puts("Origin must be #{@configuration.origin} in Cloudflare's published application settings.") unless local && connector && public_app
32
+ local && connector && public_app
33
+ end
34
+
35
+ private
36
+
37
+ def verify(label, url, expected = nil)
38
+ uri = URI(url)
39
+ # Ignore proxy environment variables for loopback diagnostics.
40
+ http = Net::HTTP.new(uri.hostname, uri.port, (uri.hostname == @configuration.hostname) ? :ENV : nil)
41
+ http.use_ssl = uri.scheme == "https"
42
+ http.open_timeout = http.read_timeout = http.write_timeout = @configuration.check_timeout
43
+ http.max_retries = 0
44
+ response = http.get(uri.request_uri, {"Cache-Control" => "no-cache"})
45
+ if response.code == "200" && (!expected || response.body == expected)
46
+ @output.puts("OK: #{label}")
47
+ true
48
+ else
49
+ @output.puts("FAIL: #{label} (HTTP #{response.code}; expected #{expected ? "this Rails application" : "an active connector"}). Check DNS, Cloudflare Access, origin and #{@configuration.log_path || "logging configuration"}.")
50
+ false
51
+ end
52
+ rescue *NETWORK_ERRORS => error
53
+ @output.puts("FAIL: #{label} (#{error.class}); network or service unavailable")
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Cloudflare
6
+ module Tunnel
7
+ class Configuration
8
+ LOG_LEVELS = %w[debug info warn error fatal].freeze
9
+ attr_accessor :enabled, :executable, :token, :hostname, :origin, :metrics_port,
10
+ :log_level, :log_path, :retry_interval, :shutdown_timeout, :check_timeout,
11
+ :root, :probe_secret
12
+
13
+ def initialize
14
+ @enabled = !%w[0 false off no].include?(ENV.fetch("TUNNEL", "1").downcase)
15
+ @executable = "cloudflared"
16
+ @root = Dir.pwd
17
+ @origin = "http://localhost:#{ENV.fetch("PORT", "3000")}"
18
+ @metrics_port = 20241
19
+ @log_level = :error
20
+ @log_path = "log/cloudflare-tunnel.log"
21
+ @retry_interval = 30
22
+ @shutdown_timeout = 5
23
+ @check_timeout = 5
24
+ end
25
+
26
+ def token_value
27
+ (token.respond_to?(:call) ? token.call : token).to_s.strip
28
+ end
29
+
30
+ def executable_path
31
+ paths = executable.to_s.include?(File::SEPARATOR) ? [File.expand_path(executable, root)] :
32
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).map { |directory| File.join(directory, executable) }
33
+ paths.find { |path| File.file?(path) && File.executable?(path) }
34
+ end
35
+
36
+ def path(value)
37
+ File.expand_path(value, root)
38
+ end
39
+
40
+ def errors
41
+ messages = []
42
+ messages << "hostname must be a bare public hostname" unless hostname.to_s.match?(/\A[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\z/i)
43
+ uri = URI.parse(origin.to_s)
44
+ unless uri.is_a?(URI::HTTP) && %w[localhost 127.0.0.1 ::1].include?(uri.hostname) &&
45
+ uri.userinfo.nil? && [nil, "", "/"].include?(uri.path) && uri.query.nil? && uri.fragment.nil?
46
+ messages << "origin must be an HTTP(S) loopback URL without a path or credentials"
47
+ end
48
+ messages << "metrics_port must be between 1 and 65535" unless metrics_port.is_a?(Integer) && (1..65535).cover?(metrics_port)
49
+ messages << "log_level must be one of #{LOG_LEVELS.join(", ")}" unless LOG_LEVELS.include?(log_level.to_s)
50
+ %i[retry_interval shutdown_timeout check_timeout].each do |name|
51
+ value = case name
52
+ when :retry_interval then retry_interval
53
+ when :shutdown_timeout then shutdown_timeout
54
+ when :check_timeout then check_timeout
55
+ end
56
+ messages << "#{name} must be a positive finite number" unless value.is_a?(Numeric) && value.finite? && value.positive?
57
+ end
58
+ messages
59
+ rescue URI::InvalidURIError
60
+ messages << "origin must be a valid loopback URL"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "rack/request"
5
+
6
+ module Cloudflare
7
+ module Tunnel
8
+ class Probe
9
+ PATH = "/.well-known/cloudflare-tunnel"
10
+
11
+ def self.signature(secret, nonce)
12
+ OpenSSL::HMAC.hexdigest("SHA256", secret, nonce)
13
+ end
14
+
15
+ def initialize(app, secret:)
16
+ @app = app
17
+ @secret = secret
18
+ end
19
+
20
+ def call(env)
21
+ request = Rack::Request.new(env)
22
+ return @app.call(env) unless request.path_info == PATH
23
+
24
+ nonce = request.GET["nonce"].to_s
25
+ if request.get? && nonce.match?(/\A[0-9a-f]{64}\z/)
26
+ [200, {"content-type" => "text/plain", "cache-control" => "no-store"}, [self.class.signature(@secret.call, nonce)]]
27
+ else
28
+ [400, {"content-type" => "text/plain", "cache-control" => "no-store"}, ["Invalid probe"]]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudflare
4
+ module Tunnel
5
+ class Railtie < Rails::Railtie
6
+ initializer "cloudflare_tunnel.defaults", before: :load_config_initializers do |app|
7
+ Cloudflare::Tunnel.configure do |config|
8
+ config.root = app.root
9
+ config.token = -> { ENV["TUNNEL_TOKEN"] || app.credentials.dig(:cloudflare, :tunnel, :token) }
10
+ config.probe_secret = -> { app.key_generator.generate_key("cloudflare-tunnel/probe", 32) }
11
+ end
12
+ end
13
+
14
+ initializer "cloudflare_tunnel.probe", after: :load_config_initializers do |app|
15
+ if Rails.env.development?
16
+ app.middleware.use Probe, secret: Cloudflare::Tunnel.configuration.probe_secret
17
+ end
18
+ end
19
+
20
+ rake_tasks do
21
+ load File.expand_path("../tasks/tunnel.rake", __dir__)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tempfile"
5
+
6
+ module Cloudflare
7
+ module Tunnel
8
+ class Runner
9
+ def initialize(configuration, output: $stdout)
10
+ @configuration = configuration
11
+ @output = output
12
+ @stopping = false
13
+ end
14
+
15
+ def run
16
+ @reader, @writer = IO.pipe
17
+ previous = %w[INT TERM].to_h { |signal| [signal, Signal.trap(signal) { stop }] }
18
+ if (reason = unavailable)
19
+ @output.puts("[tunnel] #{reason}")
20
+ pause
21
+ else
22
+ FileUtils.mkdir_p(@configuration.path("tmp/pids"))
23
+ File.open(@configuration.path("tmp/pids/cloudflare-tunnel.lock"), File::RDWR | File::CREAT, 0o600) do |lock|
24
+ if lock.flock(File::LOCK_EX | File::LOCK_NB)
25
+ supervise
26
+ else
27
+ @output.puts("[tunnel] already running for this application; skipping")
28
+ pause
29
+ end
30
+ end
31
+ end
32
+ ensure
33
+ terminate
34
+ previous&.each { |signal, handler| Signal.trap(signal, handler) }
35
+ @reader&.close
36
+ @writer&.close
37
+ end
38
+
39
+ private
40
+
41
+ def unavailable
42
+ if !@configuration.enabled
43
+ "disabled"
44
+ elsif !@configuration.executable_path
45
+ "cloudflared not found. Install it yourself: #{INSTALL_URL}"
46
+ elsif @configuration.errors.any?
47
+ "invalid configuration: #{@configuration.errors.join("; ")}"
48
+ elsif @configuration.token_value.empty?
49
+ "token missing; set cloudflare.tunnel.token in Rails credentials"
50
+ end
51
+ end
52
+
53
+ def supervise
54
+ log_path = @configuration.log_path && @configuration.path(@configuration.log_path)
55
+ FileUtils.mkdir_p(File.dirname(log_path)) if log_path
56
+ File.open(log_path || File::NULL, "a", 0o600) do |log|
57
+ Tempfile.create(["cloudflare-tunnel-", ".token"]) do |token|
58
+ token.write(@configuration.token_value)
59
+ token.flush
60
+ @output.puts("[tunnel] #{@configuration.hostname} -> #{@configuration.origin}; diagnostics: #{log_path || "disabled"}")
61
+ until @stopping
62
+ @pid = Process.spawn({"TUNNEL_TOKEN" => nil, "TUNNEL_TOKEN_FILE" => nil,
63
+ "TUNNEL_LOGFILE" => nil, "TUNNEL_LOGDIRECTORY" => nil},
64
+ @configuration.executable_path, "tunnel", "--no-autoupdate",
65
+ "--loglevel", @configuration.log_level.to_s,
66
+ "--metrics", "127.0.0.1:#{@configuration.metrics_port}",
67
+ "--grace-period", "2s", "run", "--token-file", token.path,
68
+ out: log, err: log)
69
+ @waiter = Process.detach(@pid)
70
+ pause(1) until @stopping || @waiter.join(0)
71
+ terminate
72
+ pause(@configuration.retry_interval) unless @stopping
73
+ end
74
+ ensure
75
+ terminate
76
+ end
77
+ end
78
+ end
79
+
80
+ def stop
81
+ @stopping = true
82
+ @writer.write_nonblock(".", exception: false)
83
+ end
84
+
85
+ def pause(timeout = nil)
86
+ IO.select([@reader], nil, nil, timeout) unless @stopping
87
+ end
88
+
89
+ def terminate
90
+ return unless @waiter
91
+
92
+ unless @waiter.join(0)
93
+ Process.kill("TERM", @pid)
94
+ unless @waiter.join(@configuration.shutdown_timeout)
95
+ Process.kill("KILL", @pid)
96
+ @waiter.join
97
+ end
98
+ end
99
+ rescue Errno::ESRCH
100
+ @waiter.join
101
+ ensure
102
+ @waiter = @pid = nil
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudflare
4
+ module Tunnel
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tunnel/version"
4
+ require_relative "tunnel/configuration"
5
+ require_relative "tunnel/runner"
6
+ require_relative "tunnel/probe"
7
+ require_relative "tunnel/check"
8
+
9
+ module Cloudflare
10
+ module Tunnel
11
+ INSTALL_URL = "https://developers.cloudflare.com/tunnel/downloads/"
12
+
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def self.configure
18
+ yield configuration
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative "tunnel/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cloudflare/tunnel"
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Cloudflare
6
+ module Tunnel
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ namespace "cloudflare_tunnel:install"
10
+ source_root File.expand_path("templates", __dir__)
11
+ desc "Configure a remotely-managed development tunnel; never installs cloudflared"
12
+ class_option :hostname, type: :string, required: true, desc: "Public development hostname"
13
+ class_option :port, type: :numeric, default: 3000, desc: "Local Rails port"
14
+ class_option :metrics_port, type: :numeric, default: 20241, desc: "Unique loopback connector metrics port"
15
+
16
+ def create_configuration
17
+ template "initializer.rb.tt", "config/initializers/cloudflare_tunnel.rb"
18
+ end
19
+
20
+ def configure_procfile
21
+ entry = "tunnel: bin/rails cloudflare_tunnel:start\n"
22
+ if File.exist?(File.join(destination_root, "Procfile.dev"))
23
+ content = File.read(File.join(destination_root, "Procfile.dev"))
24
+ if content.match?(/^tunnel:\s+bin\/tunnel\s*$/)
25
+ gsub_file "Procfile.dev", /^tunnel:\s+bin\/tunnel[^\n]*\n?/, entry
26
+ elsif content.match?(/^tunnel:/)
27
+ say "Existing tunnel entry retained; use: #{entry.strip}"
28
+ else
29
+ append_to_file "Procfile.dev", "\n#{entry}"
30
+ end
31
+ else
32
+ create_file "Procfile.dev", entry
33
+ end
34
+ end
35
+
36
+ def instructions
37
+ say "Set cloudflare.tunnel.token with bin/rails credentials:edit."
38
+ say "In Cloudflare, route #{options[:hostname]} to http://localhost:#{options[:port]}."
39
+ say "Install cloudflared yourself if needed: #{Cloudflare::Tunnel::INSTALL_URL}"
40
+ say "Start bin/dev, then run bin/rails cloudflare_tunnel:check."
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Rails.env.development?
4
+ Cloudflare::Tunnel.configure do |config|
5
+ config.hostname = <%= options[:hostname].inspect %>
6
+ config.origin = "http://localhost:#{ENV.fetch("PORT", <%= options[:port].to_s.inspect %>)}"
7
+ config.metrics_port = <%= options[:metrics_port] %>
8
+ config.log_level = :error
9
+ config.log_path = "log/cloudflare-tunnel.log" # nil discards connector output
10
+ config.retry_interval = 30
11
+ # config.executable = "/path/to/cloudflared"
12
+ # config.enabled = false # TUNNEL=0 also disables the default configuration
13
+ # Token defaults to TUNNEL_TOKEN or credentials.dig(:cloudflare, :tunnel, :token).
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudflare-tunnel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paulo Fidalgo
8
+ - Ethos Link
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '8.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '8.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
33
+ - !ruby/object:Gem::Dependency
34
+ name: net-http
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.9'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.9'
47
+ - !ruby/object:Gem::Dependency
48
+ name: openssl
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.3'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '5'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '3.3'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '5'
67
+ description: Supervise host-installed cloudflared with Rails configuration, quiet
68
+ retries, secure token handling, and end-to-end diagnostics.
69
+ email:
70
+ - devel@ethos-link.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - LICENSE.txt
77
+ - README.md
78
+ - lib/cloudflare-tunnel.rb
79
+ - lib/cloudflare/tasks/tunnel.rake
80
+ - lib/cloudflare/tunnel.rb
81
+ - lib/cloudflare/tunnel/check.rb
82
+ - lib/cloudflare/tunnel/configuration.rb
83
+ - lib/cloudflare/tunnel/probe.rb
84
+ - lib/cloudflare/tunnel/railtie.rb
85
+ - lib/cloudflare/tunnel/runner.rb
86
+ - lib/cloudflare/tunnel/version.rb
87
+ - lib/generators/cloudflare_tunnel/install_generator.rb
88
+ - lib/generators/cloudflare_tunnel/templates/initializer.rb.tt
89
+ homepage: https://github.com/ethos-link/cloudflare-tunnel
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ allowed_push_host: https://rubygems.org
94
+ homepage_uri: https://github.com/ethos-link/cloudflare-tunnel
95
+ source_code_uri: https://github.com/ethos-link/cloudflare-tunnel
96
+ changelog_uri: https://github.com/ethos-link/cloudflare-tunnel/blob/main/CHANGELOG.md
97
+ bug_tracker_uri: https://github.com/ethos-link/cloudflare-tunnel/issues
98
+ documentation_uri: https://github.com/ethos-link/cloudflare-tunnel/blob/main/README.md
99
+ rubygems_mfa_required: 'true'
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 4.0.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 4.0.20
115
+ specification_version: 4
116
+ summary: Quiet, offline-tolerant Cloudflare tunnels for Rails development
117
+ test_files: []