eks-cent 3.0.0 โ 4.0.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 +35 -0
- data/README.md +149 -67
- data/bin/ekscentup +82 -18
- data/config.eks +33 -27
- data/images/logo.webp +0 -0
- data/lib/eks-cent.rb +7 -2
- data/lib/eks_cent/builder.rb +17 -1
- data/lib/eks_cent/cascade.rb +24 -0
- data/lib/eks_cent/middleware/head.rb +19 -0
- data/lib/eks_cent/middleware/method_override.rb +38 -0
- data/lib/eks_cent/middleware/runtime.rb +19 -0
- data/lib/eks_cent/mock_request.rb +8 -2
- data/lib/eks_cent/mock_response.rb +47 -0
- data/lib/eks_cent/request.rb +66 -19
- data/lib/eks_cent/response.rb +37 -6
- data/lib/eks_cent/router.rb +46 -3
- data/lib/eks_cent/url_map.rb +29 -0
- data/lib/eks_cent/version.rb +1 -1
- metadata +15 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ed494c921a2bb36933450d69a731cc504340a404d3b101224a5247b81f99666
|
|
4
|
+
data.tar.gz: 8f99a004701ac50d66be5db2385dfb01ad57860eb78ce67ec38d1dbe5be2d54a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: babec8d09352ead4578f627d0e9cd69d57d234e6e2d3d6f7471884ec424354a84270ae8ad80b10f855862d973517ae8da4aaa4d59dfdb9d6d8194a3fbbe5d983
|
|
7
|
+
data.tar.gz: d77626acdc60cb91342839ac4c73c533adaa6b4d37c38c581ba23f75e5e22ea0b8246fb1e850f68ad6bc92a689573d0da272869f65199cacc6cad5f0804ec226
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog - Eks-Cent
|
|
2
|
+
|
|
3
|
+
Semua perubahan penting pada proyek ini akan didokumentasikan dalam file ini.
|
|
4
|
+
|
|
5
|
+
## [4.0.0] - 2026-03-31
|
|
6
|
+
|
|
7
|
+
Rilis Major dengan peningkatan arsitektural signifikan dan fitur standar Eks Interface.
|
|
8
|
+
|
|
9
|
+
### Ditambahkan
|
|
10
|
+
- **URL Mapping (`map`)**: Memungkinkan menjalankan beberapa aplikasi di bawah jalur URL yang berbeda.
|
|
11
|
+
- **Application Cascade**: Mekanisme fallback otomatis antar aplikasi jika terjadi 404.
|
|
12
|
+
- **Middleware Baru**:
|
|
13
|
+
- `EksCent::Middleware::Runtime`: Header `X-Runtime` untuk pelacakan performa.
|
|
14
|
+
- `EksCent::Middleware::MethodOverride`: Dukungan method HTTP non-GET/POST via parameter `_method`.
|
|
15
|
+
- `EksCent::Middleware::Head`: Penanganan otomatis permintaan `HEAD`.
|
|
16
|
+
- **Keamanan**: Batasan parameter (`EKS_QUERY_PARSER_PARAMS_LIMIT` dan `EKS_MULTIPART_TOTAL_PART_LIMIT`) untuk mitigasi serangan DoS.
|
|
17
|
+
- **Helper Respons**: Menambahkan metode `set_header` dan `content_type=` pada kelas `Response`.
|
|
18
|
+
- **Mock Testing**: Objek `MockResponse` yang lebih kaya fitur untuk pengujian unit.
|
|
19
|
+
|
|
20
|
+
### Diubah
|
|
21
|
+
- **Response Layout**: Sistem layout ERB kini mendeteksi `views/layout.erb` secara otomatis.
|
|
22
|
+
- **Error Handling**: Peningkatan UI pada middleware `ShowExceptions`.
|
|
23
|
+
- **CLI**: Perbaikan logika auto-reload pada `ekscentup -R`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [1.0.0] - [3.0.0] - 2026-03-28
|
|
28
|
+
|
|
29
|
+
Peningkatan stabilitas dan fitur pendukung rute.
|
|
30
|
+
|
|
31
|
+
### Ditambahkan
|
|
32
|
+
- Dukungan `multipart/form-data` menggunakan `Eks Standard Request` secara internal.
|
|
33
|
+
- Mekanisme `halt` di Router menggunakan `catch/throw`.
|
|
34
|
+
- Custom `not_found` dan `error` DSL di Router.
|
|
35
|
+
- Injeksi objek `@req` dan `@res` ke dalam template ERB.
|
data/README.md
CHANGED
|
@@ -1,106 +1,188 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="images/logo.webp" alt="Eks-Cent Logo" width="200" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Eks-Cent Framework v4.0.0 ๐
|
|
4
6
|
|
|
5
|
-
Eks-Cent menggunakan **
|
|
7
|
+
**Eks-Cent** adalah framework web Ruby modern yang ringan, menggunakan standar **Eks Interface**. Dirancang untuk kecepatan, keamanan tingkat tinggi, dan fleksibilitas tanpa beban dependensi eksternal yang besar. Dengan v4.0.0, Eks-Cent kini mendukung fitur arsitektural canggih seperti **URL Mapping** dan **Application Cascading**.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
---
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
- ๐ **Security Pack**:
|
|
11
|
-
- **Signed Sessions**: Data session aman dengan HMAC-SHA256.
|
|
12
|
-
- **XSS Protection**: Helper `h` otomatis untuk escape HTML di template.
|
|
13
|
-
- **Security Headers**: Middleware bawaan untuk header Frame-Options, XSS-Protection, dll.
|
|
14
|
-
- ๐จ **Templating**: Integrasi native dengan **ERB** (Embedded Ruby).
|
|
15
|
-
- ๐ฆ **JSON Ready**: Parsing otomatis untuk request body `application/json`.
|
|
16
|
-
- โก **High Performance**: Terintegrasi dengan Eksa-Server (Cluster mode & Workers).
|
|
17
|
-
- ๐ **CLI Tool**: Jalankan aplikasi dengan perintah `ekscentup` layaknya `rackup`.
|
|
11
|
+
## โจ Fitur Utama v4.0.0
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
- ๐ค **Modern Routing DSL**: Pendefinisian rute yang intuitif dengan parameter dinamis (`:name`), namespace, dan kontrol eksekusi (`halt`).
|
|
14
|
+
- ๐บ **URL Mapping & Cascading**: Jalankan beberapa aplikasi independen di bawah satu server berdasarkan sub-jalur (path mapping) atau fallback otomatis.
|
|
15
|
+
- ๐ **Security First**: Session terenkripsi HMAC-SHA256, proteksi XSS otomatis, header keamanan bawaan, dan pembatasan parameter (DoS protection).
|
|
16
|
+
- ๐ **Standard Middleware Suite**:
|
|
17
|
+
- `Runtime`: Pantau performa dengan header `X-Runtime`.
|
|
18
|
+
- `MethodOverride`: Gunakan `PUT/DELETE` dari form HTML biasa.
|
|
19
|
+
- `Head`: Otomatisasi penanganan request `HEAD`.
|
|
20
|
+
- `Session`, `Logger`, `Static`, `ShowExceptions`, dll.
|
|
21
|
+
- ๐จ **Smart Templating**: Integrasi **ERB** dengan sistem **Auto-Layout**, injeksi objek `@req`/`@res`, dan helper keamanan `h`.
|
|
22
|
+
- ๐งช **First-Class Testing**: Infrastruktur pengujian bawaan menggunakan `MockRequest` dan `MockResponse`.
|
|
23
|
+
- โก **Production Ready**: Terintegrasi penuh dengan **Eksa-Server** (Cluster mode & Workers) dan CLI tool `ekscentup`.
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
---
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
gem 'eks-cent'
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Lalu jalankan:
|
|
28
|
-
```bash
|
|
29
|
-
gem install eks-cent
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## ๐ Memulai Cepat
|
|
27
|
+
## ๐ Memulai Cepat (v4 Style)
|
|
33
28
|
|
|
34
|
-
Buat file
|
|
29
|
+
Buat file `config.eks` untuk mendefinisikan aplikasi Anda:
|
|
35
30
|
|
|
36
31
|
```ruby
|
|
37
|
-
#
|
|
38
|
-
use EksCent::Middleware::
|
|
32
|
+
# 1. Global Middlewares
|
|
33
|
+
use EksCent::Middleware::Runtime
|
|
34
|
+
use EksCent::Middleware::MethodOverride
|
|
39
35
|
use EksCent::Middleware::Session
|
|
40
36
|
use EksCent::Middleware::Logger
|
|
41
|
-
use EksCent::Middleware::ShowExceptions
|
|
42
37
|
|
|
38
|
+
# 2. Pemetaan Aplikasi API
|
|
39
|
+
map "/api" do
|
|
40
|
+
api = EksCent::Router.new do
|
|
41
|
+
get '/status' do |req, res|
|
|
42
|
+
res.content_type = 'application/json'
|
|
43
|
+
res.write({ status: 'online', version: EksCent::VERSION }.to_json)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
run api
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 3. Aplikasi Web Utama
|
|
43
50
|
router = EksCent::Router.new do
|
|
44
|
-
# Halaman Utama
|
|
45
51
|
get '/' do |req, res|
|
|
46
52
|
req.session['visits'] ||= 0
|
|
47
53
|
req.session['visits'] += 1
|
|
48
|
-
res.write "<h1>
|
|
54
|
+
res.write "<h1>Web Utama v#{EksCent::VERSION}</h1>"
|
|
55
|
+
res.write "<p>Kunjungan Anda: #{req.session['visits']}</p>"
|
|
49
56
|
end
|
|
57
|
+
end
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
run router
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Jalankan dengan perintah:
|
|
63
|
+
```bash
|
|
64
|
+
ekscentup -R --port 3000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ๐ค Dokumentasi Routing
|
|
70
|
+
|
|
71
|
+
### Router DSL
|
|
72
|
+
Anda dapat mendefinisikan rute menggunakan metode HTTP standar (`get`, `post`, `put`, `delete`, `patch`, `options`, `any`).
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
router = EksCent::Router.new do
|
|
76
|
+
get '/user/:id' do |req, res|
|
|
77
|
+
id = req.params['id']
|
|
78
|
+
res.write "User ID: #{id}"
|
|
54
79
|
end
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
res.headers['Content-Type'] = 'application/json'
|
|
60
|
-
res.write({ status: 'success', data: req.params }.to_json)
|
|
81
|
+
namespace '/admin' do
|
|
82
|
+
get '/dashboard' do |req, res|
|
|
83
|
+
# Diakses via /admin/dashboard
|
|
61
84
|
end
|
|
62
85
|
end
|
|
86
|
+
|
|
87
|
+
# Kontrol Eksekusi
|
|
88
|
+
get '/secret' do |req, res|
|
|
89
|
+
halt(403, "Akses ditolak") unless req.session['admin']
|
|
90
|
+
res.write "Data Rahasia"
|
|
91
|
+
end
|
|
63
92
|
end
|
|
93
|
+
```
|
|
64
94
|
|
|
65
|
-
|
|
95
|
+
### URL Mapping & Cascade
|
|
96
|
+
Gunakan `map` untuk membagi aplikasi besar menjadi sub-aplikasi yang lebih kecil. Gunakan `cascade` jika Anda ingin mencoba beberapa aplikasi secara bergantian hingga ada yang merespons (selain 404).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## ๐ฆ Middleware Bawaan
|
|
101
|
+
|
|
102
|
+
| Middleware | Deskripsi |
|
|
103
|
+
|------------|-----------|
|
|
104
|
+
| `EksCent::Middleware::Runtime` | Menambahkan header `X-Runtime` dengan waktu eksekusi. |
|
|
105
|
+
| `EksCent::Middleware::MethodOverride` | Mengizinkan override method HTTP via parameter `_method`. |
|
|
106
|
+
| `EksCent::Middleware::Session` | Manajemen session aman berbasis cookie dengan HMAC. |
|
|
107
|
+
| `EksCent::Middleware::Logger` | Logging permintaan ke STDOUT atau file log. |
|
|
108
|
+
| `EksCent::Middleware::Static` | Melayani file statis dari direktori tertentu (misal: `public`). |
|
|
109
|
+
| `EksCent::Middleware::ShowExceptions` | Menampilkan halaman error yang informatif saat terjadi *crash*. |
|
|
110
|
+
| `EksCent::Middleware::ContentSecurity` | Menambahkan header keamanan standar (X-Content-Type, X-Frame-Options). |
|
|
111
|
+
| `EksCent::Middleware::Head` | Mengosongkan body untuk request HEAD secara otomatis. |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## ๐จ Templating & Layout
|
|
116
|
+
|
|
117
|
+
Letakkan file `.erb` Anda di dalam direktori `views/`. Secara otomatis, framework akan mencari `views/layout.erb` sebagai pembungkus utama.
|
|
118
|
+
|
|
119
|
+
**views/layout.erb**:
|
|
120
|
+
```erb
|
|
121
|
+
<html>
|
|
122
|
+
<body>
|
|
123
|
+
<header>My App</header>
|
|
124
|
+
<%= @content %> <!-- Konten dari render akan disisipkan di sini -->
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
66
127
|
```
|
|
67
128
|
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
|
|
129
|
+
Di dalam Router:
|
|
130
|
+
```ruby
|
|
131
|
+
res.render 'index', judul: "Halo Dunia"
|
|
71
132
|
```
|
|
72
133
|
|
|
73
|
-
|
|
134
|
+
**Context Injection**: Objek `@req` (request) dan `@res` (response) serta helper `@h` (escape HTML) selalu tersedia di dalam template.
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
|------|-----------|
|
|
77
|
-
| `-p, --port` | Menentukan port server (default: 3000) |
|
|
78
|
-
| `-o, --host` | Menentukan host server (default: 0.0.0.0) |
|
|
79
|
-
| `-w, --workers` | Jumlah worker untuk mode Cluster |
|
|
80
|
-
| `-R, --reload` | Aktifkan auto-reload saat file berubah |
|
|
81
|
-
| `-e, --env` | Set lingkungan (`development` atau `production`) |
|
|
82
|
-
| `-L, --log` | Simpan log ke file tertentu |
|
|
136
|
+
---
|
|
83
137
|
|
|
84
|
-
##
|
|
138
|
+
## ๐ Panduan API (Request & Response)
|
|
85
139
|
|
|
86
|
-
|
|
140
|
+
### EksCent::Request (`req`)
|
|
141
|
+
- `req.params`: Mengambil parameter query, POST, atau route params.
|
|
142
|
+
- `req.session`: Mengakses data session (Read/Write).
|
|
143
|
+
- `req.request_method`: Mendapatkan method HTTP (GET, POST, dll).
|
|
144
|
+
- `req.path`: Mendapatkan jalur URL saat ini.
|
|
145
|
+
- `req.user_agent`: Mendapatkan informasi browser.
|
|
87
146
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
147
|
+
### EksCent::Response (`res`)
|
|
148
|
+
- `res.write(string)`: Menambahkan konten ke body respons.
|
|
149
|
+
- `res.set_header(key, value)`: Mengatur header HTTP.
|
|
150
|
+
- `res.content_type = 'type'`: Shortcut untuk mengatur Content-Type.
|
|
151
|
+
- `res.status = code`: Mengatur status code (default: 200).
|
|
152
|
+
- `res.redirect(path)`: Melakukan pengalihan URL.
|
|
153
|
+
- `res.render(template, locals)`: Merender template ERB.
|
|
93
154
|
|
|
94
|
-
|
|
155
|
+
---
|
|
95
156
|
|
|
96
|
-
|
|
157
|
+
## ๐งช Pengujian (Testing)
|
|
97
158
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
ruby
|
|
101
|
-
|
|
159
|
+
Gunakan suite pengujian bawaan untuk memastikan aplikasi Anda berjalan dengan benar:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
require 'test/unit'
|
|
163
|
+
require 'eks-cent'
|
|
164
|
+
|
|
165
|
+
class MyAppTest < Test::Unit::TestCase
|
|
166
|
+
def test_homepage
|
|
167
|
+
app = EksCent.load('config.eks')
|
|
168
|
+
mock = EksCent::MockRequest.new(app)
|
|
169
|
+
|
|
170
|
+
res = mock.get('/')
|
|
171
|
+
assert res.ok?
|
|
172
|
+
assert_match "Welcome", res.body_content
|
|
173
|
+
end
|
|
174
|
+
end
|
|
102
175
|
```
|
|
103
176
|
|
|
104
|
-
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ๐ก Keamanan Dasar (Eks Limits)
|
|
180
|
+
Anda dapat mengatur batasan penguraian parameter melalui variabel lingkungan untuk mencegah serangan DoS:
|
|
181
|
+
- `EKS_QUERY_PARSER_PARAMS_LIMIT`: Maksimal jumlah parameter (default: 1000).
|
|
182
|
+
- `EKS_QUERY_PARSER_DEPTH_LIMIT`: Maksimal kedalaman parameter nested.
|
|
183
|
+
- `EKS_MULTIPART_TOTAL_PART_LIMIT`: Maksimal part dalam form multipart.
|
|
105
184
|
|
|
106
|
-
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## ๐ Lisensi
|
|
188
|
+
Eks-Cent v4.0.0 dipublikasikan di bawah [Lisensi MIT](LICENSE).
|
data/bin/ekscentup
CHANGED
|
@@ -22,7 +22,7 @@ options = {
|
|
|
22
22
|
workers: 0,
|
|
23
23
|
reload: false,
|
|
24
24
|
timeout: 30,
|
|
25
|
-
env: ENV['RACK_ENV'] || 'development'
|
|
25
|
+
env: ENV['EKS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
OptionParser.new do |opts|
|
|
@@ -56,7 +56,7 @@ OptionParser.new do |opts|
|
|
|
56
56
|
|
|
57
57
|
opts.on("-e", "--env ENV", "Set environment (development/production)") do |v|
|
|
58
58
|
options[:env] = v
|
|
59
|
-
ENV['
|
|
59
|
+
ENV['EKS_ENV'] = v
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
opts.on("-v", "--version", "Tampilkan versi Eks-Cent") do
|
|
@@ -99,22 +99,86 @@ end
|
|
|
99
99
|
|
|
100
100
|
puts "\e[34m[Eks-Cent] Environment: #{EksCent.env}\e[0m"
|
|
101
101
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
# Fungsi untuk menjalankan server
|
|
103
|
+
def start_server(config_path, options)
|
|
104
|
+
begin
|
|
105
|
+
# Load aplikasi via Builder di setiap start agar config direfresh
|
|
106
|
+
app = EksCent.load(config_path)
|
|
107
|
+
server = EksaServerCore.new(app, options.merge(reload: false))
|
|
108
|
+
server.start
|
|
109
|
+
rescue Interrupt
|
|
110
|
+
# Biarkan parent menangani Interrupt
|
|
111
|
+
exit 0
|
|
112
|
+
rescue => e
|
|
113
|
+
puts "\e[31m[Eks-Cent] Server error: #{e.message}\e[0m"
|
|
114
|
+
puts e.backtrace.first(5).join("\n")
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
if options[:reload] && Process.respond_to?(:fork)
|
|
120
|
+
puts "\e[34m[Eks-Cent] Auto-reload aktif. Memantau perubahan file di direktori ini...\e[0m"
|
|
121
|
+
|
|
122
|
+
trap("INT") do
|
|
123
|
+
puts "\n\e[33mMenutup Reloader Eks-Cent...\e[0m"
|
|
124
|
+
Process.kill("INT", @child_pid) rescue nil if @child_pid
|
|
125
|
+
exit 0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
loop do
|
|
129
|
+
@child_pid = fork do
|
|
130
|
+
start_server(config_path, options)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Pemantau file sederhana (cek mtime setiap detik)
|
|
134
|
+
watched_extensions = %w[.rb .eks .erb .html .css .js]
|
|
135
|
+
initial_mtimes = {}
|
|
136
|
+
|
|
137
|
+
# Inisialisasi daftar file
|
|
138
|
+
Dir.glob("**/*").each do |f|
|
|
139
|
+
next unless watched_extensions.include?(File.extname(f))
|
|
140
|
+
initial_mtimes[f] = File.mtime(f) rescue nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
loop do
|
|
144
|
+
sleep 1
|
|
145
|
+
|
|
146
|
+
# Periksa perubahan
|
|
147
|
+
changed = false
|
|
148
|
+
Dir.glob("**/*").each do |f|
|
|
149
|
+
next unless watched_extensions.include?(File.extname(f))
|
|
150
|
+
current_mtime = File.mtime(f) rescue nil
|
|
151
|
+
if current_mtime && (!initial_mtimes[f] || current_mtime > initial_mtimes[f])
|
|
152
|
+
changed = true
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if changed
|
|
158
|
+
puts "\e[34m[Reloader] Perubahan terdeteksi. Me-restart server...\e[0m"
|
|
159
|
+
Process.kill("INT", @child_pid) rescue nil
|
|
160
|
+
Process.wait(@child_pid) rescue nil
|
|
161
|
+
break # Keluar ke loop untuk fork baru
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Cek jika proses anak berhenti mendadak
|
|
165
|
+
if (pid = Process.waitpid(@child_pid, Process::WNOHANG))
|
|
166
|
+
puts "\e[31m[Reloader] Server (PID: #{pid}) berhenti. Me-restart dalam 2 detik...\e[0m"
|
|
167
|
+
sleep 2
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
# Mode Standar (Tanpa Reload)
|
|
174
|
+
begin
|
|
175
|
+
app = EksCent.load(config_path)
|
|
176
|
+
server = EksaServerCore.new(app, options)
|
|
177
|
+
server.start
|
|
178
|
+
rescue Interrupt
|
|
179
|
+
puts "\n\e[33mMenutup server Eks-Cent...\e[0m"
|
|
180
|
+
rescue => e
|
|
181
|
+
puts "\e[31mServer error: #{e.message}\e[0m"
|
|
182
|
+
exit 1
|
|
183
|
+
end
|
|
120
184
|
end
|
data/config.eks
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
1
|
# File: config.eks
|
|
2
|
-
# Contoh konfigurasi untuk Eks-Cent
|
|
2
|
+
# Contoh konfigurasi lanjutan untuk Eks-Cent v4.0.0
|
|
3
3
|
|
|
4
|
+
# 1. Globals & Security Middlewares
|
|
5
|
+
use EksCent::Middleware::Runtime # Header X-Runtime otomatis
|
|
6
|
+
use EksCent::Middleware::Head # Penanganan HEAD request otomatis
|
|
7
|
+
use EksCent::Middleware::MethodOverride # Dukungan _method=DELETE/PUT via form
|
|
4
8
|
use EksCent::Middleware::ContentSecurity
|
|
5
9
|
use EksCent::Middleware::Session
|
|
6
|
-
use EksCent::Middleware::Static, root: 'public'
|
|
7
10
|
use EksCent::Middleware::Logger
|
|
8
11
|
use EksCent::Middleware::ShowExceptions
|
|
9
12
|
|
|
10
|
-
# Aplikasi
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
# 2. Aplikasi API (Dipetakan ke /api)
|
|
14
|
+
map "/api" do
|
|
15
|
+
router_api = EksCent::Router.new do
|
|
16
|
+
get "/status" do |req, res|
|
|
17
|
+
res.set_header 'Content-Type', 'application/json'
|
|
18
|
+
res.write "{\"status\": \"online\", \"version\": \"#{EksCent::VERSION}\"}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
put "/update" do |req, res|
|
|
22
|
+
res.write "Method PUT diterima (via MethodOverride). Method asli: #{req.env['eks_cent.original_method']}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
run router_api
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 3. Aplikasi Web Utama (Dipetakan ke root /)
|
|
29
|
+
router_web = EksCent::Router.new do
|
|
13
30
|
get '/' do |req, res|
|
|
14
|
-
# Contoh penggunaan session
|
|
15
31
|
req.session['visits'] ||= 0
|
|
16
32
|
req.session['visits'] += 1
|
|
17
33
|
|
|
18
|
-
res.write "<h1>Selamat Datang di Eks-Cent!</h1>"
|
|
19
|
-
res.write "<p>
|
|
20
|
-
res.write "<p>
|
|
21
|
-
res.write "<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
res.
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
get '/hello/:name' do |req, res|
|
|
30
|
-
name = req.params['name'] || 'Fulan'
|
|
31
|
-
res.write "Halo, #{name}! Senang bertemu Anda di #{req.user_agent}."
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
get '/hello' do |req, res|
|
|
35
|
-
name = req.params['name'] || 'Fulan'
|
|
36
|
-
res.write "Halo, #{name}! Senang bertemu Anda di #{req.user_agent}."
|
|
34
|
+
res.write "<h1>Selamat Datang di Eks-Cent v#{EksCent::VERSION}!</h1>"
|
|
35
|
+
res.write "<p>Fitur baru: <strong>URL Mapping</strong> & <strong>Runtime Tracking</strong>.</p>"
|
|
36
|
+
res.write "<p>Kunjungan: <strong>#{req.session['visits']}</strong></p>"
|
|
37
|
+
res.write "<hr>"
|
|
38
|
+
res.write "<ul>"
|
|
39
|
+
res.write " <li>Akses API: <a href='/api/status'>/api/status</a></li>"
|
|
40
|
+
res.write " <li>Gunakan MethodOverride: <form action='/api/update' method='post' style='display:inline'><input type='hidden' name='_method' value='PUT'><button type='submit'>Kirim PUT via POST</button></form></li>"
|
|
41
|
+
res.write " <li>Coba Error: <a href='/error'>/error</a></li>"
|
|
42
|
+
res.write "</ul>"
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
get '/error' do |req, res|
|
|
40
|
-
raise "
|
|
46
|
+
raise "Kesalahan yang disengaja untuk demonstrasi v4.0.0."
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
run
|
|
50
|
+
run router_web
|
data/images/logo.webp
ADDED
|
Binary file
|
data/lib/eks-cent.rb
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
# Eks-Cent: Lightweight
|
|
1
|
+
# Eks-Cent: Lightweight Eks-style Communication Interface for Ruby.
|
|
2
2
|
|
|
3
3
|
require_relative 'eks_cent/version'
|
|
4
4
|
require_relative 'eks_cent/request'
|
|
5
5
|
require_relative 'eks_cent/response'
|
|
6
6
|
require_relative 'eks_cent/builder'
|
|
7
7
|
require_relative 'eks_cent/router'
|
|
8
|
+
require_relative 'eks_cent/url_map'
|
|
9
|
+
require_relative 'eks_cent/cascade'
|
|
8
10
|
require_relative 'eks_cent/mock_request'
|
|
9
11
|
require_relative 'eks_cent/middleware/logger'
|
|
10
12
|
require_relative 'eks_cent/middleware/session'
|
|
11
13
|
require_relative 'eks_cent/middleware/content_security'
|
|
12
14
|
require_relative 'eks_cent/middleware/show_exceptions'
|
|
13
15
|
require_relative 'eks_cent/middleware/static'
|
|
16
|
+
require_relative 'eks_cent/middleware/method_override'
|
|
17
|
+
require_relative 'eks_cent/middleware/runtime'
|
|
18
|
+
require_relative 'eks_cent/middleware/head'
|
|
14
19
|
|
|
15
20
|
module EksCent
|
|
16
21
|
|
|
@@ -22,7 +27,7 @@ module EksCent
|
|
|
22
27
|
self.secret_key_base = ENV['EKS_CENT_SECRET_KEY_BASE'] || '1e8a93e80c85b1a6c4b69d9c2e8b2a1a8e1b1d8c1c2e1f2g1h1i1j1k1l1m1n1o'
|
|
23
28
|
|
|
24
29
|
def self.env
|
|
25
|
-
ENV['RACK_ENV'] || ENV['EKS_CENT_ENV'] || 'development'
|
|
30
|
+
ENV['EKS_ENV'] || ENV['RACK_ENV'] || ENV['EKS_CENT_ENV'] || 'development'
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def self.production?
|
data/lib/eks_cent/builder.rb
CHANGED
|
@@ -14,7 +14,23 @@ module EksCent
|
|
|
14
14
|
@run = app
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def map(path, &block)
|
|
18
|
+
@map ||= {}
|
|
19
|
+
@map[path] = self.class.new(&block).to_app
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cascade(*apps)
|
|
23
|
+
require_relative 'cascade' unless defined?(EksCent::Cascade)
|
|
24
|
+
@run = Cascade.new(apps)
|
|
25
|
+
end
|
|
26
|
+
|
|
17
27
|
def to_app
|
|
28
|
+
if @map
|
|
29
|
+
require_relative 'url_map' unless defined?(EksCent::URLMap)
|
|
30
|
+
@map['/'] = @run if @run
|
|
31
|
+
@run = URLMap.new(@map)
|
|
32
|
+
end
|
|
33
|
+
|
|
18
34
|
app = @run
|
|
19
35
|
raise "Aplikasi tidak ditemukan (run nil)" unless app
|
|
20
36
|
@use.reverse_each { |middleware| app = middleware.call(app) }
|
|
@@ -28,7 +44,7 @@ module EksCent
|
|
|
28
44
|
builder.to_app
|
|
29
45
|
end
|
|
30
46
|
|
|
31
|
-
# Helper for
|
|
47
|
+
# Helper for Eks-style interface call
|
|
32
48
|
def call(env)
|
|
33
49
|
to_app.call(env)
|
|
34
50
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
class Cascade
|
|
3
|
+
def initialize(apps = [])
|
|
4
|
+
@apps = apps
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def call(env)
|
|
8
|
+
last_response = [404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
|
|
9
|
+
|
|
10
|
+
@apps.each do |app|
|
|
11
|
+
status, headers, body = app.call(env)
|
|
12
|
+
|
|
13
|
+
# Jika bukan 404, atau header X-Cascade tidak bernilai 'pass', kembalikan respons ini
|
|
14
|
+
if status.to_i != 404 && headers['X-Cascade'] != 'pass'
|
|
15
|
+
return [status, headers, body]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
last_response = [status, headers, body]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
last_response
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class Head
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
status, headers, body = @app.call(env)
|
|
10
|
+
|
|
11
|
+
if env['REQUEST_METHOD'] == 'HEAD'
|
|
12
|
+
body = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
[status, headers, body]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class MethodOverride
|
|
4
|
+
HTTP_METHODS = %w(GET HEAD POST PUT DELETE PATCH OPTIONS LINK UNLINK)
|
|
5
|
+
METHOD_OVERRIDE_PARAM_KEY = "_method"
|
|
6
|
+
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
|
|
7
|
+
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
if env["REQUEST_METHOD"] == "POST"
|
|
14
|
+
method = method_from_env(env)
|
|
15
|
+
if method && HTTP_METHODS.include?(method.upcase)
|
|
16
|
+
env["eks_cent.original_method"] = env["REQUEST_METHOD"]
|
|
17
|
+
env["REQUEST_METHOD"] = method.upcase
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@app.call(env)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def method_from_env(env)
|
|
27
|
+
# 1. Cek dari header X-HTTP-Method-Override
|
|
28
|
+
return env[HTTP_METHOD_OVERRIDE_HEADER] if env[HTTP_METHOD_OVERRIDE_HEADER]
|
|
29
|
+
|
|
30
|
+
# 2. Cek dari parameter body (_method)
|
|
31
|
+
req = Request.new(env)
|
|
32
|
+
return req.params[METHOD_OVERRIDE_PARAM_KEY] if req.params[METHOD_OVERRIDE_PARAM_KEY]
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class Runtime
|
|
4
|
+
def initialize(app, header_name = 'X-Runtime')
|
|
5
|
+
@app = app
|
|
6
|
+
@header_name = header_name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
start_time = Time.now
|
|
11
|
+
status, headers, body = @app.call(env)
|
|
12
|
+
duration = Time.now - start_time
|
|
13
|
+
|
|
14
|
+
headers[@header_name] = format("%0.6f", duration)
|
|
15
|
+
[status, headers, body]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -21,10 +21,16 @@ module EksCent
|
|
|
21
21
|
'PATH_INFO' => path,
|
|
22
22
|
'QUERY_STRING' => query || '',
|
|
23
23
|
'HTTP_USER_AGENT' => opts[:user_agent] || 'EksCent-MockRequest',
|
|
24
|
-
'rack.input' => StringIO.new(opts[:body] || '')
|
|
25
24
|
}.merge(opts[:env] || {})
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
env['eks.input'] ||= StringIO.new(opts[:body] || '')
|
|
27
|
+
env['rack.input'] ||= env['eks.input']
|
|
28
|
+
env['eks.version'] ||= [1, 3]
|
|
29
|
+
env['rack.version'] ||= env['eks.version']
|
|
30
|
+
|
|
31
|
+
status, headers, body = @app.call(env)
|
|
32
|
+
require_relative 'mock_response' unless defined?(EksCent::MockResponse)
|
|
33
|
+
MockResponse.new(status, headers, body)
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
class MockResponse
|
|
3
|
+
attr_reader :status, :headers, :body
|
|
4
|
+
|
|
5
|
+
def initialize(status, headers, body)
|
|
6
|
+
@status = status.to_i
|
|
7
|
+
@headers = headers
|
|
8
|
+
@body = body
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def body_content
|
|
12
|
+
@body.is_a?(Array) ? @body.join : @body.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ok?
|
|
16
|
+
@status == 200
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def redirect?
|
|
20
|
+
[301, 302, 303, 307, 308].include?(@status)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def client_error?
|
|
24
|
+
@status >= 400 && @status < 500
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def server_error?
|
|
28
|
+
@status >= 500 && @status < 600
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def not_found?
|
|
32
|
+
@status == 404
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def location
|
|
36
|
+
@headers['Location']
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def content_type
|
|
40
|
+
@headers['Content-Type']
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_ary
|
|
44
|
+
[@status, @headers, @body]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/eks_cent/request.rb
CHANGED
|
@@ -8,6 +8,19 @@ module EksCent
|
|
|
8
8
|
|
|
9
9
|
def initialize(env)
|
|
10
10
|
@env = env
|
|
11
|
+
# Map standard Rack keys to Eks branding for internal consistency
|
|
12
|
+
@env['eks.input'] ||= @env['rack.input']
|
|
13
|
+
@env['rack.input'] ||= @env['eks.input']
|
|
14
|
+
|
|
15
|
+
@env['eks.version'] ||= @env['rack.version'] || [1, 3]
|
|
16
|
+
@env['rack.version'] ||= @env['eks.version']
|
|
17
|
+
|
|
18
|
+
@env['eks.errors'] ||= @env['rack.errors']
|
|
19
|
+
@env['rack.errors'] ||= @env['eks.errors']
|
|
20
|
+
|
|
21
|
+
@env['eks.multithread'] ||= @env['rack.multithread'] || false
|
|
22
|
+
@env['eks.multiprocess'] ||= @env['rack.multiprocess'] || false
|
|
23
|
+
@env['eks.run_once'] ||= @env['rack.run_once'] || false
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
def request_method
|
|
@@ -56,37 +69,71 @@ module EksCent
|
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
def parse_params
|
|
59
|
-
|
|
72
|
+
require 'rack' unless defined?(Rack)
|
|
60
73
|
|
|
61
|
-
#
|
|
62
|
-
|
|
74
|
+
# Terapkan batasan global Eks/Rack jika ada di ENV
|
|
75
|
+
setup_eks_limits
|
|
76
|
+
|
|
77
|
+
# Gunakan Rack::Request untuk menangani parsing parameter standar (GET/POST/Multipart)
|
|
78
|
+
rack_req = Rack::Request.new(@env)
|
|
79
|
+
params = rack_req.params.dup
|
|
63
80
|
|
|
64
|
-
#
|
|
81
|
+
# Gabungkan dengan parameter dari router (misal: /hello/:name)
|
|
65
82
|
if @env['eks_cent.router_params']
|
|
66
83
|
params.merge!(@env['eks_cent.router_params'])
|
|
67
84
|
end
|
|
68
85
|
|
|
69
|
-
#
|
|
70
|
-
if @env['
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
# Tambahkan parsing JSON manual karena Rack::Request tidak melakukannya secara otomatis
|
|
87
|
+
if @env['CONTENT_TYPE'] == 'application/json' && @env['eks.input']
|
|
88
|
+
begin
|
|
89
|
+
body = @env['eks.input'].read
|
|
90
|
+
@env['eks.input'].rewind if @env['eks.input'].respond_to?(:rewind)
|
|
91
|
+
|
|
92
|
+
if body && !body.empty?
|
|
93
|
+
json_params = JSON.parse(body)
|
|
94
|
+
if json_params.is_a?(Hash)
|
|
95
|
+
# Batasi jumlah parameter JSON jika EKS/RACK_QUERY_PARSER_PARAMS_LIMIT diatur
|
|
96
|
+
limit = (ENV['EKS_QUERY_PARSER_PARAMS_LIMIT'] || ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'])&.to_i || 1000
|
|
97
|
+
if json_params.size > limit
|
|
98
|
+
raise "Too many parameters (JSON)"
|
|
99
|
+
end
|
|
100
|
+
params.merge!(json_params)
|
|
80
101
|
end
|
|
81
|
-
else
|
|
82
|
-
# Default to form-urlencoded if it's a POST/PUT/PATCH
|
|
83
|
-
params.merge!(CGI.parse(body)) if post? || @env['REQUEST_METHOD'] == 'PUT' || @env['REQUEST_METHOD'] == 'PATCH'
|
|
84
102
|
end
|
|
103
|
+
rescue JSON::ParserError
|
|
104
|
+
# Abaikan error parsing JSON
|
|
85
105
|
end
|
|
86
106
|
end
|
|
87
107
|
|
|
88
|
-
#
|
|
108
|
+
# Pastikan nilai parameter diratakan (flatten) jika berupa array berukuran 1
|
|
109
|
+
# Catatan: Rack::Request mungkin sudah melakukannya untuk form data standar,
|
|
110
|
+
# tapi kita pastikan konsistensi di sini.
|
|
89
111
|
params.transform_values { |v| v.is_a?(Array) && v.size == 1 ? v.first : v }
|
|
90
112
|
end
|
|
113
|
+
def setup_eks_limits
|
|
114
|
+
return if @eks_limits_setup
|
|
115
|
+
|
|
116
|
+
parser = Rack::Utils.default_query_parser rescue nil
|
|
117
|
+
return unless parser
|
|
118
|
+
|
|
119
|
+
params_limit = ENV['EKS_QUERY_PARSER_PARAMS_LIMIT'] || ENV['RACK_QUERY_PARSER_PARAMS_LIMIT']
|
|
120
|
+
if params_limit && parser.respond_to?(:params_limit=)
|
|
121
|
+
parser.params_limit = params_limit.to_i
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
bytesize_limit = ENV['EKS_QUERY_PARSER_BYTESIZE_LIMIT'] || ENV['RACK_QUERY_PARSER_BYTESIZE_LIMIT']
|
|
125
|
+
if bytesize_limit && parser.respond_to?(:bytesize_limit=)
|
|
126
|
+
parser.bytesize_limit = bytesize_limit.to_i
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
multipart_limit = ENV['EKS_MULTIPART_TOTAL_PART_LIMIT'] || ENV['RACK_MULTIPART_TOTAL_PART_LIMIT']
|
|
130
|
+
if multipart_limit
|
|
131
|
+
if Rack::Utils.respond_to?(:multipart_total_part_limit=)
|
|
132
|
+
Rack::Utils.multipart_total_part_limit = multipart_limit.to_i
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@eks_limits_setup = true
|
|
137
|
+
end
|
|
91
138
|
end
|
|
92
139
|
end
|
data/lib/eks_cent/response.rb
CHANGED
|
@@ -3,18 +3,27 @@ require 'time'
|
|
|
3
3
|
|
|
4
4
|
module EksCent
|
|
5
5
|
class Response
|
|
6
|
-
attr_accessor :status, :headers, :body
|
|
6
|
+
attr_accessor :status, :headers, :body, :request
|
|
7
7
|
|
|
8
|
-
def initialize(body = [], status = 200, headers = {})
|
|
8
|
+
def initialize(body = [], status = 200, headers = {}, request: nil)
|
|
9
9
|
@status = status
|
|
10
10
|
@headers = { 'Content-Type' => 'text/html' }.merge(headers)
|
|
11
11
|
@body = body.is_a?(String) ? [body] : body
|
|
12
|
+
@request = request
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def write(str)
|
|
15
16
|
@body << str
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
def set_header(key, value)
|
|
20
|
+
@headers[key] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def content_type=(type)
|
|
24
|
+
@headers['Content-Type'] = type
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def redirect(target, status = 302)
|
|
19
28
|
@status = status
|
|
20
29
|
@headers['Location'] = target
|
|
@@ -25,7 +34,7 @@ module EksCent
|
|
|
25
34
|
@cookies[name] = options.merge(value: value)
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
def render(template_name,
|
|
37
|
+
def render(template_name, layout: true, **locals)
|
|
29
38
|
require 'erb'
|
|
30
39
|
template_path = File.join('views', "#{template_name}.erb")
|
|
31
40
|
unless File.file?(template_path)
|
|
@@ -37,12 +46,34 @@ module EksCent
|
|
|
37
46
|
# Gunakan context khusus agar helper h (escape HTML) tersedia
|
|
38
47
|
context = Object.new
|
|
39
48
|
context.extend(ERB::Util)
|
|
49
|
+
|
|
50
|
+
# Suntikkan objek internal
|
|
51
|
+
context.instance_variable_set("@req", @request) if @request
|
|
52
|
+
context.instance_variable_set("@res", self)
|
|
53
|
+
|
|
54
|
+
# Suntikkan locals
|
|
40
55
|
locals.each { |k, v| context.instance_variable_set("@#{k}", v) }
|
|
41
56
|
|
|
42
|
-
# Definisikan metode helper h secara eksplisit
|
|
43
|
-
def context.h(s);
|
|
57
|
+
# Definisikan metode helper h secara eksplisit untuk keamanan
|
|
58
|
+
def context.h(s); CGI.escapeHTML(s.to_s); end
|
|
59
|
+
|
|
60
|
+
# Render template utama
|
|
61
|
+
result = ERB.new(template_content).result(context.instance_eval { binding })
|
|
62
|
+
|
|
63
|
+
# Dukungan Layout (default mencari views/layout.erb)
|
|
64
|
+
if layout
|
|
65
|
+
layout_name = layout == true ? 'layout' : layout.to_s
|
|
66
|
+
layout_path = File.join('views', "#{layout_name}.erb")
|
|
67
|
+
|
|
68
|
+
if File.file?(layout_path)
|
|
69
|
+
context.instance_variable_set("@content", result)
|
|
70
|
+
layout_content = File.read(layout_path)
|
|
71
|
+
result = ERB.new(layout_content).result(context.instance_eval { binding })
|
|
72
|
+
end
|
|
73
|
+
end
|
|
44
74
|
|
|
45
|
-
@
|
|
75
|
+
@headers['Content-Type'] ||= 'text/html'
|
|
76
|
+
@body << result
|
|
46
77
|
end
|
|
47
78
|
|
|
48
79
|
def finish
|
data/lib/eks_cent/router.rb
CHANGED
|
@@ -10,9 +10,25 @@ module EksCent
|
|
|
10
10
|
}
|
|
11
11
|
@prefix = ''
|
|
12
12
|
@current_middlewares = []
|
|
13
|
+
@not_found_block = nil
|
|
14
|
+
@error_block = nil
|
|
13
15
|
instance_eval(&block) if block_given?
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
def not_found(&block)
|
|
19
|
+
@not_found_block = block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error(&block)
|
|
23
|
+
@error_block = block
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def halt(res, status = nil, body = nil)
|
|
27
|
+
res.status = status if status
|
|
28
|
+
res.write(body) if body
|
|
29
|
+
throw(:halt)
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
def get(path, &block) add_route('GET', path, &block) end
|
|
17
33
|
def post(path, &block) add_route('POST', path, &block) end
|
|
18
34
|
def put(path, &block) add_route('PUT', path, &block) end
|
|
@@ -52,8 +68,19 @@ module EksCent
|
|
|
52
68
|
# Wrapped application with route-specific middlewares
|
|
53
69
|
app = proc do |e|
|
|
54
70
|
req_i = Request.new(e)
|
|
55
|
-
res_i = Response.new
|
|
56
|
-
|
|
71
|
+
res_i = Response.new(request: req_i)
|
|
72
|
+
begin
|
|
73
|
+
catch(:halt) do
|
|
74
|
+
instance_exec(req_i, res_i, &route[:block])
|
|
75
|
+
end
|
|
76
|
+
rescue => err
|
|
77
|
+
if @error_block
|
|
78
|
+
res_i.status = 500
|
|
79
|
+
instance_exec(err, req_i, res_i, &@error_block)
|
|
80
|
+
else
|
|
81
|
+
raise err
|
|
82
|
+
end
|
|
83
|
+
end
|
|
57
84
|
res_i.finish
|
|
58
85
|
end
|
|
59
86
|
route[:middlewares].reverse_each { |m| app = m.new(app) }
|
|
@@ -61,12 +88,28 @@ module EksCent
|
|
|
61
88
|
_status, headers, body = app.call(env)
|
|
62
89
|
[_status, headers, body]
|
|
63
90
|
else
|
|
64
|
-
|
|
91
|
+
handle_not_found(env)
|
|
65
92
|
end
|
|
66
93
|
end
|
|
67
94
|
|
|
68
95
|
private
|
|
69
96
|
|
|
97
|
+
def handle_not_found(env)
|
|
98
|
+
req = Request.new(env)
|
|
99
|
+
res = Response.new(request: req)
|
|
100
|
+
res.status = 404
|
|
101
|
+
|
|
102
|
+
if @not_found_block
|
|
103
|
+
instance_exec(req, res, &@not_found_block)
|
|
104
|
+
res.finish
|
|
105
|
+
elsif File.exist?(File.join('views', '404.erb'))
|
|
106
|
+
res.render '404'
|
|
107
|
+
res.finish
|
|
108
|
+
else
|
|
109
|
+
[404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
70
113
|
def add_route(method, path, &block)
|
|
71
114
|
keys = []
|
|
72
115
|
full_path = "#{@prefix}#{path}".gsub('//', '/')
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
class URLMap
|
|
3
|
+
def initialize(map = {})
|
|
4
|
+
@mapping = map.map do |path, app|
|
|
5
|
+
[path.chomp('/'), app]
|
|
6
|
+
end.sort_by { |path, _| -path.length } # Sort by longest path first
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
path_info = env['PATH_INFO'] || ''
|
|
11
|
+
script_name = env['SCRIPT_NAME'] || ''
|
|
12
|
+
|
|
13
|
+
@mapping.each do |path, app|
|
|
14
|
+
next unless path_info.start_with?(path)
|
|
15
|
+
next unless path_info == path || path_info[path.length] == '/'
|
|
16
|
+
|
|
17
|
+
# Matched path: shift script_name and path_info
|
|
18
|
+
new_env = env.dup
|
|
19
|
+
new_env['SCRIPT_NAME'] = script_name + path
|
|
20
|
+
new_env['PATH_INFO'] = path_info[path.length..-1].to_s
|
|
21
|
+
new_env['PATH_INFO'] = '/' if new_env['PATH_INFO'].empty?
|
|
22
|
+
|
|
23
|
+
return app.call(new_env)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
[404, { 'Content-Type' => 'text/plain', 'X-Cascade' => 'pass' }, ["Not Found (URLMap)"]]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/eks_cent/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eks-cent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- IshikawaUta
|
|
@@ -27,16 +27,16 @@ dependencies:
|
|
|
27
27
|
name: base64
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
32
|
+
version: '0.2'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
39
|
+
version: '0.2'
|
|
40
40
|
description: Eks-Cent adalah framework web minimalis yang menyediakan sistem routing
|
|
41
41
|
canggih, manajemen session HMAC, dan proteksi keamanan bawaan menggunakan Eksa-Server
|
|
42
42
|
sebagai engine utama.
|
|
@@ -47,29 +47,37 @@ executables:
|
|
|
47
47
|
extensions: []
|
|
48
48
|
extra_rdoc_files: []
|
|
49
49
|
files:
|
|
50
|
+
- CHANGELOG.md
|
|
50
51
|
- LICENSE
|
|
51
52
|
- README.md
|
|
52
53
|
- bin/ekscentup
|
|
53
54
|
- config.eks
|
|
55
|
+
- images/logo.webp
|
|
54
56
|
- lib/eks-cent.rb
|
|
55
57
|
- lib/eks_cent/builder.rb
|
|
58
|
+
- lib/eks_cent/cascade.rb
|
|
56
59
|
- lib/eks_cent/middleware/content_security.rb
|
|
60
|
+
- lib/eks_cent/middleware/head.rb
|
|
57
61
|
- lib/eks_cent/middleware/logger.rb
|
|
62
|
+
- lib/eks_cent/middleware/method_override.rb
|
|
63
|
+
- lib/eks_cent/middleware/runtime.rb
|
|
58
64
|
- lib/eks_cent/middleware/session.rb
|
|
59
65
|
- lib/eks_cent/middleware/show_exceptions.rb
|
|
60
66
|
- lib/eks_cent/middleware/static.rb
|
|
61
67
|
- lib/eks_cent/mock_request.rb
|
|
68
|
+
- lib/eks_cent/mock_response.rb
|
|
62
69
|
- lib/eks_cent/request.rb
|
|
63
70
|
- lib/eks_cent/response.rb
|
|
64
71
|
- lib/eks_cent/router.rb
|
|
72
|
+
- lib/eks_cent/url_map.rb
|
|
65
73
|
- lib/eks_cent/version.rb
|
|
66
74
|
homepage: https://github.com/IshikawaUta/eks-cent
|
|
67
75
|
licenses:
|
|
68
76
|
- MIT
|
|
69
77
|
metadata:
|
|
70
78
|
allowed_push_host: https://rubygems.org
|
|
71
|
-
homepage_uri: https://github.com/IshikawaUta/eks-cent
|
|
72
79
|
source_code_uri: https://github.com/IshikawaUta/eks-cent
|
|
80
|
+
changelog_uri: https://github.com/IshikawaUta/eks-cent/blob/main/CHANGELOG.md
|
|
73
81
|
rdoc_options: []
|
|
74
82
|
require_paths:
|
|
75
83
|
- lib
|
|
@@ -86,5 +94,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
94
|
requirements: []
|
|
87
95
|
rubygems_version: 3.6.7
|
|
88
96
|
specification_version: 4
|
|
89
|
-
summary: Framework web Ruby ringan, aman, dan siap produksi berbasis
|
|
97
|
+
summary: Framework web Ruby ringan, aman, dan siap produksi berbasis Eks Interface.
|
|
90
98
|
test_files: []
|