tayo 0.2.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +38 -79
- data/docs/2025-11-29-cf-command-refactor.md +53 -0
- data/lib/tayo/cli.rb +4 -4
- data/lib/tayo/commands/cf.rb +137 -30
- data/lib/tayo/commands/gh.rb +90 -55
- data/lib/tayo/commands/init.rb +15 -2
- data/lib/tayo/commands/sqlite.rb +371 -0
- data/lib/tayo/templates/solid-cable-sqlite-setup.md +90 -0
- data/lib/tayo/version.rb +1 -1
- metadata +5 -10
- data/lib/tayo/commands/proxy.rb +0 -97
- data/lib/tayo/proxy/cloudflare_client.rb +0 -323
- data/lib/tayo/proxy/docker_manager.rb +0 -150
- data/lib/tayo/proxy/network_config.rb +0 -147
- data/lib/tayo/proxy/traefik_config.rb +0 -303
- data/lib/tayo/proxy/welcome_service.rb +0 -337
- data/lib/templates/welcome/Dockerfile +0 -14
- data/lib/templates/welcome/index.html +0 -173
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Solid Cable + SQLite 최적화 설정
|
|
2
|
+
|
|
3
|
+
> 이 문서는 `tayo sqlite` 명령어로 자동 생성되었습니다.
|
|
4
|
+
|
|
5
|
+
## 개요
|
|
6
|
+
|
|
7
|
+
이 프로젝트는 Redis 없이 SQLite만으로 Action Cable 실시간 기능을 구현합니다.
|
|
8
|
+
Rails 8에서 도입된 Solid Cable을 사용하여 WebSocket 브로드캐스트를 처리합니다.
|
|
9
|
+
|
|
10
|
+
## 왜 Solid Cable인가?
|
|
11
|
+
|
|
12
|
+
### Redis vs Solid Cable
|
|
13
|
+
|
|
14
|
+
| 항목 | Redis | Solid Cable |
|
|
15
|
+
|------|-------|-------------|
|
|
16
|
+
| 의존성 | Redis 서버 필요 | DB만 사용 |
|
|
17
|
+
| 방식 | Pub/Sub (푸시) | Polling (폴링) |
|
|
18
|
+
| 지연시간 | ~1ms | ~25ms (설정값) |
|
|
19
|
+
| 운영 복잡도 | Redis 관리 필요 | 단순 |
|
|
20
|
+
|
|
21
|
+
### 체감 성능
|
|
22
|
+
|
|
23
|
+
- **25ms 폴링**: 사용자가 지연을 체감하기 어려움 (200ms 이하는 "즉각적"으로 느껴짐)
|
|
24
|
+
- **RTT 비교**: Redis ~80ms vs Solid Cable ~85ms (전체 왕복 시간 기준, 거의 동일)
|
|
25
|
+
|
|
26
|
+
## 적용된 설정
|
|
27
|
+
|
|
28
|
+
### 1. 데이터베이스 분리 (database.yml)
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
production:
|
|
32
|
+
primary:
|
|
33
|
+
database: storage/production.sqlite3
|
|
34
|
+
cable:
|
|
35
|
+
database: storage/production_cable.sqlite3 # 별도 DB
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**이유**: Cable 폴링이 메인 DB의 쓰기 작업과 락 경합하지 않도록 분리
|
|
39
|
+
|
|
40
|
+
### 2. Cable 설정 (cable.yml)
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
development:
|
|
44
|
+
adapter: async # 단일 프로세스, 콘솔 디버깅 용이
|
|
45
|
+
|
|
46
|
+
production:
|
|
47
|
+
adapter: solid_cable
|
|
48
|
+
polling_interval: 0.025.seconds # 25ms
|
|
49
|
+
message_retention: 1.hour
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**폴링 간격 선택 기준**:
|
|
53
|
+
- 100ms: 기본값, 대부분 충분
|
|
54
|
+
- 25ms: 채팅/실시간 앱에 적합
|
|
55
|
+
- 10ms: Redis 수준, 부하 증가
|
|
56
|
+
|
|
57
|
+
### 3. SQLite WAL 모드 최적화 (initializer)
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# config/initializers/solid_cable_sqlite.rb
|
|
61
|
+
PRAGMA journal_mode=WAL # 읽기/쓰기 동시 처리
|
|
62
|
+
PRAGMA synchronous=NORMAL # 쓰기 성능 향상
|
|
63
|
+
PRAGMA cache_size=4000 # 캐시 증가
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**WAL 모드 장점**:
|
|
67
|
+
- 읽기(폴링)와 쓰기(브로드캐스트)가 동시에 가능
|
|
68
|
+
- Solid Cable의 폴링 패턴에 최적화
|
|
69
|
+
|
|
70
|
+
## 성능 가이드
|
|
71
|
+
|
|
72
|
+
### 적정 사용 범위
|
|
73
|
+
|
|
74
|
+
| 동시 접속 | 메시지/초 | 예상 지연 |
|
|
75
|
+
|----------|----------|----------|
|
|
76
|
+
| ~50명 | ~10 | 거의 없음 |
|
|
77
|
+
| ~100명 | ~20 | ~50ms |
|
|
78
|
+
| ~500명 | ~100 | 검토 필요 |
|
|
79
|
+
|
|
80
|
+
### 스케일 아웃이 필요한 경우
|
|
81
|
+
|
|
82
|
+
- 초당 200개 이상 메시지
|
|
83
|
+
- 1000명 이상 동시 접속
|
|
84
|
+
- → Redis 또는 PostgreSQL NOTIFY 고려
|
|
85
|
+
|
|
86
|
+
## 참고 자료
|
|
87
|
+
|
|
88
|
+
- [Solid Cable GitHub](https://github.com/rails/solid_cable)
|
|
89
|
+
- [SQLite WAL Mode](https://sqlite.org/wal.html)
|
|
90
|
+
- [Rails 8 Release Notes](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required)
|
data/lib/tayo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tayo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 이원섭wonsup Lee/Alfonso
|
|
@@ -92,22 +92,17 @@ files:
|
|
|
92
92
|
- CHANGELOG.md
|
|
93
93
|
- README.md
|
|
94
94
|
- Rakefile
|
|
95
|
+
- docs/2025-11-29-cf-command-refactor.md
|
|
95
96
|
- exe/tayo
|
|
96
97
|
- lib/tayo.rb
|
|
97
98
|
- lib/tayo/cli.rb
|
|
98
99
|
- lib/tayo/commands/cf.rb
|
|
99
100
|
- lib/tayo/commands/gh.rb
|
|
100
101
|
- lib/tayo/commands/init.rb
|
|
101
|
-
- lib/tayo/commands/
|
|
102
|
+
- lib/tayo/commands/sqlite.rb
|
|
102
103
|
- lib/tayo/dockerfile_modifier.rb
|
|
103
|
-
- lib/tayo/
|
|
104
|
-
- lib/tayo/proxy/docker_manager.rb
|
|
105
|
-
- lib/tayo/proxy/network_config.rb
|
|
106
|
-
- lib/tayo/proxy/traefik_config.rb
|
|
107
|
-
- lib/tayo/proxy/welcome_service.rb
|
|
104
|
+
- lib/tayo/templates/solid-cable-sqlite-setup.md
|
|
108
105
|
- lib/tayo/version.rb
|
|
109
|
-
- lib/templates/welcome/Dockerfile
|
|
110
|
-
- lib/templates/welcome/index.html
|
|
111
106
|
- pkg/homebody-0.1.0.gem
|
|
112
107
|
- repomix-output.xml
|
|
113
108
|
- sig/tayo.rbs
|
|
@@ -131,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
126
|
- !ruby/object:Gem::Version
|
|
132
127
|
version: '0'
|
|
133
128
|
requirements: []
|
|
134
|
-
rubygems_version:
|
|
129
|
+
rubygems_version: 4.0.3
|
|
135
130
|
specification_version: 4
|
|
136
131
|
summary: Rails deployment tool for home servers
|
|
137
132
|
test_files: []
|
data/lib/tayo/commands/proxy.rb
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "colorize"
|
|
4
|
-
require "tty-prompt"
|
|
5
|
-
require_relative "../proxy/cloudflare_client"
|
|
6
|
-
require_relative "../proxy/docker_manager"
|
|
7
|
-
require_relative "../proxy/network_config"
|
|
8
|
-
require_relative "../proxy/traefik_config"
|
|
9
|
-
require_relative "../proxy/welcome_service"
|
|
10
|
-
|
|
11
|
-
module Tayo
|
|
12
|
-
module Commands
|
|
13
|
-
class Proxy
|
|
14
|
-
def execute
|
|
15
|
-
puts "🚀 Kamal Proxy와 Caddy 설정을 시작합니다...".colorize(:green)
|
|
16
|
-
puts ""
|
|
17
|
-
|
|
18
|
-
# 1. Cloudflare 설정
|
|
19
|
-
cloudflare = Tayo::Proxy::CloudflareClient.new
|
|
20
|
-
cloudflare.ensure_token
|
|
21
|
-
|
|
22
|
-
# 2. 네트워크 설정
|
|
23
|
-
network = Tayo::Proxy::NetworkConfig.new
|
|
24
|
-
network.detect_ips
|
|
25
|
-
network.configure_ports
|
|
26
|
-
|
|
27
|
-
# 3. Docker 확인
|
|
28
|
-
docker = Tayo::Proxy::DockerManager.new
|
|
29
|
-
docker.check_containers
|
|
30
|
-
|
|
31
|
-
# 4. 도메인 선택
|
|
32
|
-
selected_domains = cloudflare.select_domains
|
|
33
|
-
|
|
34
|
-
if selected_domains.empty?
|
|
35
|
-
puts "❌ 도메인이 선택되지 않았습니다. 종료합니다.".colorize(:red)
|
|
36
|
-
return
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# 5. DNS 설정
|
|
40
|
-
puts "\n📝 DNS 레코드를 설정합니다...".colorize(:yellow)
|
|
41
|
-
cloudflare.setup_dns_records(selected_domains, network.public_ip)
|
|
42
|
-
|
|
43
|
-
# 6. Welcome 서비스 확인
|
|
44
|
-
welcome = Tayo::Proxy::WelcomeService.new
|
|
45
|
-
welcome.ensure_running
|
|
46
|
-
|
|
47
|
-
# 7. Traefik 설정
|
|
48
|
-
traefik = Tayo::Proxy::TraefikConfig.new
|
|
49
|
-
traefik.setup(selected_domains)
|
|
50
|
-
|
|
51
|
-
# 8. 최종 안내
|
|
52
|
-
show_summary(selected_domains, network)
|
|
53
|
-
|
|
54
|
-
rescue => e
|
|
55
|
-
puts "❌ 오류가 발생했습니다: #{e.message}".colorize(:red)
|
|
56
|
-
puts e.backtrace.join("\n") if ENV["DEBUG"]
|
|
57
|
-
exit 1
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def show_summary(domains, network)
|
|
63
|
-
puts "\n" + "="*60
|
|
64
|
-
puts "✅ Proxy 설정이 완료되었습니다!".colorize(:green)
|
|
65
|
-
puts "="*60
|
|
66
|
-
|
|
67
|
-
puts "\n📋 설정 요약:".colorize(:yellow)
|
|
68
|
-
puts "━"*40
|
|
69
|
-
puts "공인 IP: #{network.public_ip}".colorize(:white)
|
|
70
|
-
puts "내부 IP: #{network.internal_ip}".colorize(:white)
|
|
71
|
-
puts "Traefik: 80, 443 포트 사용 중".colorize(:white)
|
|
72
|
-
puts "대시보드: http://localhost:8080".colorize(:white)
|
|
73
|
-
puts "━"*40
|
|
74
|
-
|
|
75
|
-
puts "\n🌐 활성화된 도메인:".colorize(:yellow)
|
|
76
|
-
domains.each do |domain|
|
|
77
|
-
if network.use_custom_ports?
|
|
78
|
-
puts "• #{domain}".colorize(:cyan)
|
|
79
|
-
puts " HTTP: http://#{domain}:#{network.external_http}".colorize(:gray)
|
|
80
|
-
puts " HTTPS: https://#{domain}:#{network.external_https}".colorize(:gray)
|
|
81
|
-
else
|
|
82
|
-
puts "• #{domain}".colorize(:cyan)
|
|
83
|
-
puts " HTTP: http://#{domain}".colorize(:gray)
|
|
84
|
-
puts " HTTPS: https://#{domain}".colorize(:gray)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
if network.use_custom_ports?
|
|
89
|
-
puts "\n💡 공유기 포트포워딩을 설정하세요!".colorize(:yellow)
|
|
90
|
-
network.show_port_forwarding_guide
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
puts "\n🎉 모든 설정이 완료되었습니다!".colorize(:green)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "colorize"
|
|
4
|
-
require "tty-prompt"
|
|
5
|
-
require "net/http"
|
|
6
|
-
require "json"
|
|
7
|
-
require "uri"
|
|
8
|
-
require "fileutils"
|
|
9
|
-
|
|
10
|
-
module Tayo
|
|
11
|
-
module Proxy
|
|
12
|
-
class CloudflareClient
|
|
13
|
-
TOKEN_FILE = File.expand_path("~/.tayo/cloudflare_token")
|
|
14
|
-
|
|
15
|
-
def initialize
|
|
16
|
-
@prompt = TTY::Prompt.new
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def ensure_token
|
|
20
|
-
@token = load_saved_token
|
|
21
|
-
|
|
22
|
-
if @token
|
|
23
|
-
puts "🔑 저장된 Cloudflare 토큰을 확인합니다...".colorize(:yellow)
|
|
24
|
-
|
|
25
|
-
if test_token(@token)
|
|
26
|
-
puts "✅ 저장된 Cloudflare 토큰이 유효합니다.".colorize(:green)
|
|
27
|
-
else
|
|
28
|
-
puts "⚠️ 저장된 토큰이 만료되었거나 유효하지 않습니다.".colorize(:yellow)
|
|
29
|
-
puts "새 토큰을 입력해주세요.".colorize(:cyan)
|
|
30
|
-
open_token_creation_page
|
|
31
|
-
@token = get_cloudflare_token
|
|
32
|
-
save_token(@token)
|
|
33
|
-
end
|
|
34
|
-
else
|
|
35
|
-
puts "\n🔑 Cloudflare API 토큰이 필요합니다.".colorize(:yellow)
|
|
36
|
-
open_token_creation_page
|
|
37
|
-
@token = get_cloudflare_token
|
|
38
|
-
save_token(@token)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def select_domains
|
|
43
|
-
puts "\n🌐 Cloudflare 도메인 목록을 조회합니다...".colorize(:yellow)
|
|
44
|
-
|
|
45
|
-
zones = get_cloudflare_zones
|
|
46
|
-
|
|
47
|
-
if zones.empty?
|
|
48
|
-
puts "❌ Cloudflare에 등록된 도메인이 없습니다.".colorize(:red)
|
|
49
|
-
puts "먼저 https://dash.cloudflare.com 에서 도메인을 추가해주세요.".colorize(:cyan)
|
|
50
|
-
return []
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
|
|
54
|
-
|
|
55
|
-
selected_zones = @prompt.multi_select(
|
|
56
|
-
"Caddy로 라우팅할 도메인을 선택하세요 (Space로 선택, Enter로 완료):",
|
|
57
|
-
zone_choices,
|
|
58
|
-
per_page: 10,
|
|
59
|
-
min: 1
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# 선택된 항목이 없으면 빈 배열 반환
|
|
63
|
-
return [] if selected_zones.nil? || selected_zones.empty?
|
|
64
|
-
|
|
65
|
-
puts "DEBUG: selected_zones = #{selected_zones.inspect}" if ENV["DEBUG"]
|
|
66
|
-
|
|
67
|
-
selected_domains = selected_zones.map do |selection|
|
|
68
|
-
zone_name = selection.split(' ').first
|
|
69
|
-
zone = zones.find { |z| z['name'] == zone_name }
|
|
70
|
-
|
|
71
|
-
# 서브도메인 추가 여부 확인
|
|
72
|
-
if @prompt.yes?("#{zone_name}에 서브도메인을 추가하시겠습니까?")
|
|
73
|
-
subdomain = @prompt.ask("서브도메인을 입력하세요 (예: app, api):")
|
|
74
|
-
if subdomain && !subdomain.empty?
|
|
75
|
-
{
|
|
76
|
-
domain: "#{subdomain}.#{zone_name}",
|
|
77
|
-
zone_id: zone['id'],
|
|
78
|
-
zone_name: zone_name
|
|
79
|
-
}
|
|
80
|
-
else
|
|
81
|
-
{
|
|
82
|
-
domain: zone_name,
|
|
83
|
-
zone_id: zone['id'],
|
|
84
|
-
zone_name: zone_name
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
else
|
|
88
|
-
{
|
|
89
|
-
domain: zone_name,
|
|
90
|
-
zone_id: zone['id'],
|
|
91
|
-
zone_name: zone_name
|
|
92
|
-
}
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
puts "\n✅ 선택된 도메인:".colorize(:green)
|
|
97
|
-
selected_domains.each do |d|
|
|
98
|
-
puts " • #{d[:domain]}".colorize(:cyan)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
puts "DEBUG: returning selected_domains = #{selected_domains.inspect}" if ENV["DEBUG"]
|
|
102
|
-
|
|
103
|
-
selected_domains
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def setup_dns_records(domains, public_ip)
|
|
107
|
-
domains.each do |domain_info|
|
|
108
|
-
domain = domain_info[:domain]
|
|
109
|
-
zone_id = domain_info[:zone_id]
|
|
110
|
-
|
|
111
|
-
puts "\n⚙️ #{domain}의 DNS 레코드를 설정합니다...".colorize(:yellow)
|
|
112
|
-
|
|
113
|
-
# 기존 A 레코드 확인
|
|
114
|
-
existing_records = get_dns_records(zone_id, domain, ['A'])
|
|
115
|
-
|
|
116
|
-
if existing_records.any?
|
|
117
|
-
existing_record = existing_records.first
|
|
118
|
-
if existing_record['content'] == public_ip
|
|
119
|
-
puts "✅ #{domain} → #{public_ip} (이미 설정됨)".colorize(:green)
|
|
120
|
-
else
|
|
121
|
-
puts "⚠️ 기존 레코드를 업데이트합니다.".colorize(:yellow)
|
|
122
|
-
update_dns_record(zone_id, existing_record['id'], public_ip)
|
|
123
|
-
end
|
|
124
|
-
else
|
|
125
|
-
# 새 A 레코드 생성
|
|
126
|
-
create_dns_record(zone_id, domain, 'A', public_ip)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def load_saved_token
|
|
134
|
-
return nil unless File.exist?(TOKEN_FILE)
|
|
135
|
-
File.read(TOKEN_FILE).strip
|
|
136
|
-
rescue
|
|
137
|
-
nil
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def save_token(token)
|
|
141
|
-
dir = File.dirname(TOKEN_FILE)
|
|
142
|
-
|
|
143
|
-
# 디렉토리가 파일로 존재하는 경우 삭제
|
|
144
|
-
if File.exist?(dir) && !File.directory?(dir)
|
|
145
|
-
FileUtils.rm(dir)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# 디렉토리가 없을 때만 생성
|
|
149
|
-
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
150
|
-
|
|
151
|
-
File.write(TOKEN_FILE, token)
|
|
152
|
-
File.chmod(0600, TOKEN_FILE)
|
|
153
|
-
puts "✅ API 토큰이 저장되었습니다.".colorize(:green)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def open_token_creation_page
|
|
157
|
-
puts "토큰 생성 페이지를 엽니다...".colorize(:cyan)
|
|
158
|
-
system("open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null || xdg-open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null")
|
|
159
|
-
|
|
160
|
-
puts "\n다음 권한으로 토큰을 생성해주세요:".colorize(:yellow)
|
|
161
|
-
puts ""
|
|
162
|
-
puts "한국어 화면:".colorize(:gray)
|
|
163
|
-
puts "• 영역 → 영역 → 읽기".colorize(:white)
|
|
164
|
-
puts "• 영역 → DNS → 편집".colorize(:white)
|
|
165
|
-
puts " (영역 리소스는 '모든 영역' 선택)".colorize(:gray)
|
|
166
|
-
puts ""
|
|
167
|
-
puts "English:".colorize(:gray)
|
|
168
|
-
puts "• Zone → Zone → Read".colorize(:white)
|
|
169
|
-
puts "• Zone → DNS → Edit".colorize(:white)
|
|
170
|
-
puts " (Zone Resources: Select 'All zones')".colorize(:gray)
|
|
171
|
-
puts ""
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def get_cloudflare_token
|
|
175
|
-
token = @prompt.mask("생성된 Cloudflare API 토큰을 붙여넣으세요:")
|
|
176
|
-
|
|
177
|
-
if token.nil? || token.strip.empty?
|
|
178
|
-
puts "❌ 토큰이 입력되지 않았습니다.".colorize(:red)
|
|
179
|
-
exit 1
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
if test_token(token.strip)
|
|
183
|
-
puts "✅ 토큰이 확인되었습니다.".colorize(:green)
|
|
184
|
-
return token.strip
|
|
185
|
-
else
|
|
186
|
-
puts "❌ 토큰이 올바르지 않거나 권한이 부족합니다.".colorize(:red)
|
|
187
|
-
exit 1
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def test_token(token)
|
|
192
|
-
uri = URI('https://api.cloudflare.com/client/v4/user/tokens/verify')
|
|
193
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
194
|
-
http.use_ssl = true
|
|
195
|
-
|
|
196
|
-
request = Net::HTTP::Get.new(uri)
|
|
197
|
-
request['Authorization'] = "Bearer #{token}"
|
|
198
|
-
request['Content-Type'] = 'application/json'
|
|
199
|
-
|
|
200
|
-
response = http.request(request)
|
|
201
|
-
|
|
202
|
-
if response.code == '200'
|
|
203
|
-
data = JSON.parse(response.body)
|
|
204
|
-
return data['success'] == true
|
|
205
|
-
else
|
|
206
|
-
return false
|
|
207
|
-
end
|
|
208
|
-
rescue => e
|
|
209
|
-
puts "⚠️ 토큰 검증 중 오류: #{e.message}".colorize(:yellow) if ENV["DEBUG"]
|
|
210
|
-
false
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def get_cloudflare_zones
|
|
214
|
-
uri = URI('https://api.cloudflare.com/client/v4/zones')
|
|
215
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
216
|
-
http.use_ssl = true
|
|
217
|
-
|
|
218
|
-
request = Net::HTTP::Get.new(uri)
|
|
219
|
-
request['Authorization'] = "Bearer #{@token}"
|
|
220
|
-
request['Content-Type'] = 'application/json'
|
|
221
|
-
|
|
222
|
-
response = http.request(request)
|
|
223
|
-
|
|
224
|
-
if response.code == '200'
|
|
225
|
-
data = JSON.parse(response.body)
|
|
226
|
-
return data['result'] || []
|
|
227
|
-
else
|
|
228
|
-
puts "❌ 도메인 목록 조회에 실패했습니다: #{response.code}".colorize(:red)
|
|
229
|
-
[]
|
|
230
|
-
end
|
|
231
|
-
rescue => e
|
|
232
|
-
puts "❌ API 요청 중 오류가 발생했습니다: #{e.message}".colorize(:red)
|
|
233
|
-
[]
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def get_dns_records(zone_id, name, types)
|
|
237
|
-
records = []
|
|
238
|
-
|
|
239
|
-
types.each do |type|
|
|
240
|
-
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
241
|
-
uri.query = URI.encode_www_form({
|
|
242
|
-
type: type,
|
|
243
|
-
name: name
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
247
|
-
http.use_ssl = true
|
|
248
|
-
|
|
249
|
-
request = Net::HTTP::Get.new(uri)
|
|
250
|
-
request['Authorization'] = "Bearer #{@token}"
|
|
251
|
-
request['Content-Type'] = 'application/json'
|
|
252
|
-
|
|
253
|
-
response = http.request(request)
|
|
254
|
-
|
|
255
|
-
if response.code == '200'
|
|
256
|
-
data = JSON.parse(response.body)
|
|
257
|
-
records.concat(data['result'] || [])
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
records
|
|
262
|
-
rescue => e
|
|
263
|
-
puts "⚠️ DNS 레코드 조회 중 오류: #{e.message}".colorize(:yellow)
|
|
264
|
-
[]
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def create_dns_record(zone_id, name, type, content)
|
|
268
|
-
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
269
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
270
|
-
http.use_ssl = true
|
|
271
|
-
|
|
272
|
-
request = Net::HTTP::Post.new(uri)
|
|
273
|
-
request['Authorization'] = "Bearer #{@token}"
|
|
274
|
-
request['Content-Type'] = 'application/json'
|
|
275
|
-
|
|
276
|
-
data = {
|
|
277
|
-
type: type,
|
|
278
|
-
name: name,
|
|
279
|
-
content: content,
|
|
280
|
-
ttl: 300,
|
|
281
|
-
proxied: false
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
request.body = data.to_json
|
|
285
|
-
response = http.request(request)
|
|
286
|
-
|
|
287
|
-
if response.code == '200'
|
|
288
|
-
puts "✅ #{name} → #{content} (A 레코드 생성됨)".colorize(:green)
|
|
289
|
-
else
|
|
290
|
-
puts "❌ DNS 레코드 생성 실패: #{response.code}".colorize(:red)
|
|
291
|
-
puts response.body if ENV["DEBUG"]
|
|
292
|
-
end
|
|
293
|
-
rescue => e
|
|
294
|
-
puts "❌ DNS 레코드 생성 중 오류: #{e.message}".colorize(:red)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def update_dns_record(zone_id, record_id, new_content)
|
|
298
|
-
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records/#{record_id}")
|
|
299
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
300
|
-
http.use_ssl = true
|
|
301
|
-
|
|
302
|
-
request = Net::HTTP::Patch.new(uri)
|
|
303
|
-
request['Authorization'] = "Bearer #{@token}"
|
|
304
|
-
request['Content-Type'] = 'application/json'
|
|
305
|
-
|
|
306
|
-
data = {
|
|
307
|
-
content: new_content
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
request.body = data.to_json
|
|
311
|
-
response = http.request(request)
|
|
312
|
-
|
|
313
|
-
if response.code == '200'
|
|
314
|
-
puts "✅ DNS 레코드가 업데이트되었습니다.".colorize(:green)
|
|
315
|
-
else
|
|
316
|
-
puts "❌ DNS 레코드 업데이트 실패: #{response.code}".colorize(:red)
|
|
317
|
-
end
|
|
318
|
-
rescue => e
|
|
319
|
-
puts "❌ DNS 레코드 업데이트 중 오류: #{e.message}".colorize(:red)
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
end
|