eks-cent 1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/bin/ekscentup +120 -0
- data/config.eks +44 -0
- data/lib/eks-cent.rb +41 -0
- data/lib/eks_cent/builder.rb +36 -0
- data/lib/eks_cent/middleware/content_security.rb +21 -0
- data/lib/eks_cent/middleware/logger.rb +20 -0
- data/lib/eks_cent/middleware/session.rb +90 -0
- data/lib/eks_cent/middleware/show_exceptions.rb +55 -0
- data/lib/eks_cent/middleware/static.rb +54 -0
- data/lib/eks_cent/mock_request.rb +30 -0
- data/lib/eks_cent/request.rb +92 -0
- data/lib/eks_cent/response.rb +75 -0
- data/lib/eks_cent/router.rb +95 -0
- data/lib/eks_cent/version.rb +3 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 73efb3a1a1ad1d549803f630114430b8434017ea8baff4e8caf07155e0c97772
|
|
4
|
+
data.tar.gz: a26be3a7a6be66e1da635495af7c2f99e12914512cea12c46caa59365f865eb8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7b82cd30fcb60e6aa144a37ebd0d16820079a210b633e249bd534eb2e08d7d9da6ce65c1e39516c17de2373a05f9c23058684bf8d914ef53611c3c09e0de5f07
|
|
7
|
+
data.tar.gz: a26da376408cbe2b99a928ff78ca78928fa3d8f0ffe790a7bf6b947e4cab3396f108f5d762bd8f4953510a53b6cc5884a1243e21581aa0f8a01092ecfe754124
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IshikawaUta
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Eks-Cent Framework 🚀
|
|
2
|
+
|
|
3
|
+
**Eks-Cent** adalah framework web Ruby ringan yang terinspirasi oleh Rack. Dirancang untuk kecepatan, keamanan, dan kemudahan penggunaan tanpa dependensi eksternal yang berat.
|
|
4
|
+
|
|
5
|
+
Eks-Cent menggunakan **Eksa-Server** sebagai engine server bawaan untuk performa tinggi di lingkungan produksi.
|
|
6
|
+
|
|
7
|
+
## ✨ Fitur Utama
|
|
8
|
+
|
|
9
|
+
- 🛤 **Advanced Routing**: DSL rute yang bersih dengan dukungan parameter dinamis dan namespace.
|
|
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`.
|
|
18
|
+
|
|
19
|
+
## 📦 Instalasi
|
|
20
|
+
|
|
21
|
+
Tambahkan baris ini ke dalam Gemfile aplikasi Anda:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'eks-cent'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Lalu jalankan:
|
|
28
|
+
```bash
|
|
29
|
+
gem install eks-cent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🚀 Memulai Cepat
|
|
33
|
+
|
|
34
|
+
Buat file bernama `config.eks`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# config.eks
|
|
38
|
+
use EksCent::Middleware::ContentSecurity
|
|
39
|
+
use EksCent::Middleware::Session
|
|
40
|
+
use EksCent::Middleware::Logger
|
|
41
|
+
use EksCent::Middleware::ShowExceptions
|
|
42
|
+
|
|
43
|
+
router = EksCent::Router.new do
|
|
44
|
+
# Halaman Utama
|
|
45
|
+
get '/' do |req, res|
|
|
46
|
+
req.session['visits'] ||= 0
|
|
47
|
+
req.session['visits'] += 1
|
|
48
|
+
res.write "<h1>Halo! Anda sudah berkunjung #{req.session['visits']} kali.</h1>"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Rute Dinamis
|
|
52
|
+
get '/hello/:name' do |req, res|
|
|
53
|
+
res.render 'welcome', name: req.params['name']
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# API JSON
|
|
57
|
+
namespace '/api' do
|
|
58
|
+
post '/data' do |req, res|
|
|
59
|
+
res.headers['Content-Type'] = 'application/json'
|
|
60
|
+
res.write({ status: 'success', data: req.params }.to_json)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
run router
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Jalankan server:
|
|
69
|
+
```bash
|
|
70
|
+
./bin/ekscentup -p 3000
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 🛠 Penggunaan CLI (`ekscentup`)
|
|
74
|
+
|
|
75
|
+
| Opsi | Deskripsi |
|
|
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 |
|
|
83
|
+
|
|
84
|
+
## 🛡 Mode Produksi
|
|
85
|
+
|
|
86
|
+
Untuk keamanan maksimal di produksi, pastikan Anda menyetel environment variable dan secret key:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export EKS_CENT_SECRET_KEY_BASE="kunci_rahasia_anda_yang_unik"
|
|
90
|
+
export RACK_ENV=production
|
|
91
|
+
./bin/ekscentup -p 80 -e production
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 🧪 Pengujian
|
|
95
|
+
|
|
96
|
+
Eks-Cent dilengkapi dengan suite pengujian yang lengkap. Untuk menjalankan semua tes:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
ruby test/eks_cent_test.rb
|
|
100
|
+
ruby test/router_test.rb
|
|
101
|
+
ruby test/security_test.rb
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 📄 Lisensi
|
|
105
|
+
|
|
106
|
+
Eks-Cent didistribusikan di bawah [Lisensi MIT](LICENSE).
|
data/bin/ekscentup
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# File: bin/ekscentup
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require_relative '../lib/eks-cent'
|
|
7
|
+
|
|
8
|
+
# Cari lokasi gem eksa-server agar bisa memuat server.rb
|
|
9
|
+
begin
|
|
10
|
+
spec = Gem::Specification.find_by_name('eksa-server')
|
|
11
|
+
gem_root = spec.gem_dir
|
|
12
|
+
require File.join(gem_root, 'server.rb')
|
|
13
|
+
rescue Gem::LoadError
|
|
14
|
+
puts "\e[31mError: Gem 'eksa-server' tidak ditemukan.\e[0m"
|
|
15
|
+
puts "Silakan instal dengan: gem install eksa-server"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
options = {
|
|
20
|
+
port: 3000,
|
|
21
|
+
host: '0.0.0.0',
|
|
22
|
+
workers: 0,
|
|
23
|
+
reload: false,
|
|
24
|
+
timeout: 30,
|
|
25
|
+
env: ENV['RACK_ENV'] || 'development'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
OptionParser.new do |opts|
|
|
29
|
+
opts.banner = "Penggunaan: ekscentup [opsi] [file_konfigurasi]"
|
|
30
|
+
opts.separator ""
|
|
31
|
+
opts.separator "Opsi Eks-Cent:"
|
|
32
|
+
|
|
33
|
+
opts.on("-p", "--port PORT", Integer, "Port untuk server (default: 3000)") do |v|
|
|
34
|
+
options[:port] = v
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on("-o", "--host HOST", "Host untuk server (default: 0.0.0.0)") do |v|
|
|
38
|
+
options[:host] = v
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on("-w", "--workers COUNT", Integer, "Jumlah worker (untuk cluster mode)") do |v|
|
|
42
|
+
options[:workers] = v
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on("-R", "--reload", "Aktifkan auto-reload saat file berubah") do
|
|
46
|
+
options[:reload] = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on("-L", "--log PATH", "Path untuk file log") do |v|
|
|
50
|
+
options[:log_file] = v
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.on("-D", "--daemonize", "Jalankan di latar belakang") do
|
|
54
|
+
options[:daemonize] = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.on("-e", "--env ENV", "Set environment (development/production)") do |v|
|
|
58
|
+
options[:env] = v
|
|
59
|
+
ENV['RACK_ENV'] = v
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on("-v", "--version", "Tampilkan versi Eks-Cent") do
|
|
63
|
+
puts "Eks-Cent versi #{EksCent::VERSION}"
|
|
64
|
+
puts "Eksa-Server versi #{EksaServer::VERSION}" if defined?(EksaServer::VERSION)
|
|
65
|
+
exit
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
opts.on("-h", "--help", "Tampilkan pesan bantuan ini") do
|
|
69
|
+
puts opts
|
|
70
|
+
exit
|
|
71
|
+
end
|
|
72
|
+
end.parse!
|
|
73
|
+
|
|
74
|
+
# File konfigurasi default adalah config.eks
|
|
75
|
+
config_file = ARGV[0] || 'config.eks'
|
|
76
|
+
config_path = File.expand_path(config_file)
|
|
77
|
+
|
|
78
|
+
unless File.exist?(config_path)
|
|
79
|
+
puts "\e[31mError: File '#{config_file}' tidak ditemukan.\e[0m"
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts "\e[32m[Eks-Cent] Memuat aplikasi dari #{config_file}...\e[0m"
|
|
84
|
+
|
|
85
|
+
# Set logger global EksCent jika ada opsi -L
|
|
86
|
+
if options[:log_file]
|
|
87
|
+
EksCent.logger = File.open(options[:log_file], 'a')
|
|
88
|
+
EksCent.logger.sync = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check Secret Key di mode produksi
|
|
92
|
+
if EksCent.production?
|
|
93
|
+
default_key = '1e8a93e80c85b1a6c4b69d9c2e8b2a1a8e1b1d8c1c2e1f2g1h1i1j1k1l1m1n1o'
|
|
94
|
+
if EksCent.secret_key_base == default_key
|
|
95
|
+
puts "\e[33m[PERINGATAN] Aplikasi berjalan di mode PRODUCTION dengan SECRET_KEY_BASE default.\e[0m"
|
|
96
|
+
puts "\e[33mSegera atur EKS_CENT_SECRET_KEY_BASE di environment variable Anda untuk keamanan!\e[0m"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
puts "\e[34m[Eks-Cent] Environment: #{EksCent.env}\e[0m"
|
|
101
|
+
|
|
102
|
+
# Load aplikasi via Builder
|
|
103
|
+
begin
|
|
104
|
+
app = EksCent.load(config_path)
|
|
105
|
+
rescue => e
|
|
106
|
+
puts "\e[31mGagal memuat aplikasi: #{e.message}\e[0m"
|
|
107
|
+
puts e.backtrace.first(5).join("\n")
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Jalankan menggunakan EksaServerCore
|
|
112
|
+
begin
|
|
113
|
+
server = EksaServerCore.new(app, options)
|
|
114
|
+
server.start
|
|
115
|
+
rescue Interrupt
|
|
116
|
+
puts "\n\e[33mMenutup server Eks-Cent...\e[0m"
|
|
117
|
+
rescue => e
|
|
118
|
+
puts "\e[31mServer error: #{e.message}\e[0m"
|
|
119
|
+
exit 1
|
|
120
|
+
end
|
data/config.eks
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# File: config.eks
|
|
2
|
+
# Contoh konfigurasi untuk Eks-Cent
|
|
3
|
+
|
|
4
|
+
use EksCent::Middleware::ContentSecurity
|
|
5
|
+
use EksCent::Middleware::Session
|
|
6
|
+
use EksCent::Middleware::Static, root: 'public'
|
|
7
|
+
use EksCent::Middleware::Logger
|
|
8
|
+
use EksCent::Middleware::ShowExceptions
|
|
9
|
+
|
|
10
|
+
# Aplikasi Utama
|
|
11
|
+
# Aplikasi Utama menggunakan Router
|
|
12
|
+
router = EksCent::Router.new do
|
|
13
|
+
get '/' do |req, res|
|
|
14
|
+
# Contoh penggunaan session
|
|
15
|
+
req.session['visits'] ||= 0
|
|
16
|
+
req.session['visits'] += 1
|
|
17
|
+
|
|
18
|
+
res.write "<h1>Selamat Datang di Eks-Cent!</h1>"
|
|
19
|
+
res.write "<p>Ini adalah framework tanpa dependensi.</p>"
|
|
20
|
+
res.write "<p>Jumlah kunjungan Anda: <strong>#{req.session['visits']}</strong></p>"
|
|
21
|
+
res.write "<p>Cobalah akses <a href='/error'>/error</a> untuk melihat middleware ShowExceptions.</p>"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/welcome' do |req, res|
|
|
25
|
+
# Contoh penggunaan ERB rendering dan h helper
|
|
26
|
+
res.render 'test', name: req.params['name'] || 'Tamu'
|
|
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}."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
get '/error' do |req, res|
|
|
40
|
+
raise "Waduh! Ada kesalahan yang disengaja di sini."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
run router
|
data/lib/eks-cent.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Eks-Cent: Lightweight Rack-like Communication Interface for Ruby.
|
|
2
|
+
|
|
3
|
+
require_relative 'eks_cent/version'
|
|
4
|
+
require_relative 'eks_cent/request'
|
|
5
|
+
require_relative 'eks_cent/response'
|
|
6
|
+
require_relative 'eks_cent/builder'
|
|
7
|
+
require_relative 'eks_cent/router'
|
|
8
|
+
require_relative 'eks_cent/mock_request'
|
|
9
|
+
require_relative 'eks_cent/middleware/logger'
|
|
10
|
+
require_relative 'eks_cent/middleware/session'
|
|
11
|
+
require_relative 'eks_cent/middleware/content_security'
|
|
12
|
+
require_relative 'eks_cent/middleware/show_exceptions'
|
|
13
|
+
require_relative 'eks_cent/middleware/static'
|
|
14
|
+
|
|
15
|
+
module EksCent
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
attr_accessor :logger, :secret_key_base
|
|
19
|
+
end
|
|
20
|
+
self.logger = $stdout
|
|
21
|
+
# Default secret key (sebaiknya diatur via ENV di produksi)
|
|
22
|
+
self.secret_key_base = ENV['EKS_CENT_SECRET_KEY_BASE'] || '1e8a93e80c85b1a6c4b69d9c2e8b2a1a8e1b1d8c1c2e1f2g1h1i1j1k1l1m1n1o'
|
|
23
|
+
|
|
24
|
+
def self.env
|
|
25
|
+
ENV['RACK_ENV'] || ENV['EKS_CENT_ENV'] || 'development'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.production?
|
|
29
|
+
env == 'production'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Helper to create a new app directly
|
|
33
|
+
def self.build(&block)
|
|
34
|
+
Builder.new(&block).to_app
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Helper to load from file
|
|
38
|
+
def self.load(file)
|
|
39
|
+
Builder.parse_file(file)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
class Builder
|
|
3
|
+
def initialize(&block)
|
|
4
|
+
@use = []
|
|
5
|
+
@run = nil
|
|
6
|
+
instance_eval(&block) if block_given?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def use(middleware, *args, **kwargs, &block)
|
|
10
|
+
@use << proc { |app| middleware.new(app, *args, **kwargs, &block) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(app)
|
|
14
|
+
@run = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_app
|
|
18
|
+
app = @run
|
|
19
|
+
raise "Aplikasi tidak ditemukan (run nil)" unless app
|
|
20
|
+
@use.reverse_each { |middleware| app = middleware.call(app) }
|
|
21
|
+
app
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.parse_file(file)
|
|
25
|
+
content = File.read(file)
|
|
26
|
+
builder = new
|
|
27
|
+
builder.instance_eval(content, file)
|
|
28
|
+
builder.to_app
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Helper for Rack-like interface call
|
|
32
|
+
def call(env)
|
|
33
|
+
to_app.call(env)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class ContentSecurity
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
status, headers, body = @app.call(env)
|
|
10
|
+
|
|
11
|
+
# Security headers
|
|
12
|
+
headers['X-Frame-Options'] ||= 'SAMEORIGIN'
|
|
13
|
+
headers['X-XSS-Protection'] ||= '1; mode=block'
|
|
14
|
+
headers['X-Content-Type-Options'] ||= 'nosniff'
|
|
15
|
+
headers['Referrer-Policy'] ||= 'strict-origin-when-cross-origin'
|
|
16
|
+
|
|
17
|
+
[status, headers, body]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class Logger
|
|
4
|
+
def initialize(app, output = EksCent.logger)
|
|
5
|
+
@app = app
|
|
6
|
+
@output = output
|
|
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
|
+
@output.puts "[EksCent] #{Time.now} | #{env['REQUEST_METHOD']} #{env['PATH_INFO']} | Status: #{status} | Duration: #{'%.4f' % duration}s"
|
|
15
|
+
|
|
16
|
+
[status, headers, body]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module EksCent
|
|
6
|
+
module Middleware
|
|
7
|
+
class Session
|
|
8
|
+
SESSION_KEY = 'eks_cent.session'
|
|
9
|
+
|
|
10
|
+
def initialize(app, options = {})
|
|
11
|
+
@app = app
|
|
12
|
+
@cookie_name = options[:cookie_name] || '_eks_cent_session'
|
|
13
|
+
@secret = options[:secret] # Placeholder for future signing
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
# 1. Load session from cookie
|
|
18
|
+
load_session(env)
|
|
19
|
+
|
|
20
|
+
# 2. Call the app
|
|
21
|
+
status, headers, body = @app.call(env)
|
|
22
|
+
|
|
23
|
+
# 3. Save session back to cookie if data exists
|
|
24
|
+
if env[SESSION_KEY]
|
|
25
|
+
json_data = JSON.dump(env[SESSION_KEY])
|
|
26
|
+
encoded_data = Base64.strict_encode64(json_data)
|
|
27
|
+
signed_data = sign(encoded_data)
|
|
28
|
+
|
|
29
|
+
cookie = "#{@cookie_name}=#{signed_data}; path=/; HttpOnly"
|
|
30
|
+
|
|
31
|
+
if headers['Set-Cookie']
|
|
32
|
+
headers['Set-Cookie'] = [headers['Set-Cookie'], cookie].flatten
|
|
33
|
+
else
|
|
34
|
+
headers['Set-Cookie'] = cookie
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[status, headers, body]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def load_session(env)
|
|
44
|
+
cookie_header = env['HTTP_COOKIE']
|
|
45
|
+
session_data = nil
|
|
46
|
+
|
|
47
|
+
if cookie_header
|
|
48
|
+
cookies = CGI::Cookie.parse(cookie_header)
|
|
49
|
+
if cookies[@cookie_name] && !cookies[@cookie_name].empty?
|
|
50
|
+
begin
|
|
51
|
+
signed_data = cookies[@cookie_name].first
|
|
52
|
+
encoded_data = verify(signed_data)
|
|
53
|
+
|
|
54
|
+
if encoded_data
|
|
55
|
+
session_data = JSON.parse(Base64.decode64(encoded_data))
|
|
56
|
+
end
|
|
57
|
+
rescue
|
|
58
|
+
session_data = {}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
env[SESSION_KEY] = session_data || {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def sign(data)
|
|
67
|
+
secret = EksCent.secret_key_base
|
|
68
|
+
signature = OpenSSL::HMAC.hexdigest('SHA256', secret, data)
|
|
69
|
+
"#{data}--#{signature}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def verify(signed_data)
|
|
73
|
+
data, signature = signed_data.split('--')
|
|
74
|
+
return nil unless data && signature
|
|
75
|
+
|
|
76
|
+
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', EksCent.secret_key_base, data)
|
|
77
|
+
|
|
78
|
+
# Gunakan constant time comparison jika tersedia (Rack::Utils),
|
|
79
|
+
# jika tidak gunakan perbandingan standar dengan aman.
|
|
80
|
+
begin
|
|
81
|
+
return data if Rack::Utils.secure_compare(signature, expected_signature)
|
|
82
|
+
rescue
|
|
83
|
+
return data if signature == expected_signature
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
module Middleware
|
|
3
|
+
class ShowExceptions
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
begin
|
|
10
|
+
@app.call(env)
|
|
11
|
+
rescue => e
|
|
12
|
+
if EksCent.production?
|
|
13
|
+
render_production_error
|
|
14
|
+
else
|
|
15
|
+
render_exception(e)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def render_production_error
|
|
23
|
+
[500, { 'Content-Type' => 'text/html' }, ["<html><body><h1>500 Internal Server Error</h1><p>Maaf, terjadi kesalahan pada server kami. Sila hubungi tim support jika masalah berlanjut.</p></body></html>"]]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_exception(e)
|
|
27
|
+
status = 500
|
|
28
|
+
headers = { 'Content-Type' => 'text/html' }
|
|
29
|
+
body = [<<-HTML]
|
|
30
|
+
<!DOCTYPE html>
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<title>Eks-Cent: Gagal Diproses</title>
|
|
34
|
+
<style>
|
|
35
|
+
body { font-family: 'Inter', sans-serif; background: #fafafa; color: #d32f2f; padding: 20px; }
|
|
36
|
+
.container { background: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 30px; border-left: 5px solid #d32f2f; }
|
|
37
|
+
h1 { color: #d32f2f; }
|
|
38
|
+
pre { background: #eee; padding: 15px; border-radius: 4px; overflow-x: auto; color: #333; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div class="container">
|
|
43
|
+
<h1>#{e.class}: #{e.message}</h1>
|
|
44
|
+
<p><strong>Stack Trace:</strong></p>
|
|
45
|
+
<pre>#{e.backtrace.join("\n")}</pre>
|
|
46
|
+
</div>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
49
|
+
HTML
|
|
50
|
+
|
|
51
|
+
[status, headers, body]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module EksCent
|
|
4
|
+
module Middleware
|
|
5
|
+
class Static
|
|
6
|
+
MIME_TYPES = {
|
|
7
|
+
'.html' => 'text/html',
|
|
8
|
+
'.css' => 'text/css',
|
|
9
|
+
'.js' => 'application/javascript',
|
|
10
|
+
'.png' => 'image/png',
|
|
11
|
+
'.jpg' => 'image/jpeg',
|
|
12
|
+
'.gif' => 'image/gif',
|
|
13
|
+
'.ico' => 'image/x-icon',
|
|
14
|
+
'.txt' => 'text/plain'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def initialize(app, root: 'public')
|
|
18
|
+
@app = app
|
|
19
|
+
@root = root
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
path = env['PATH_INFO']
|
|
24
|
+
|
|
25
|
+
# Prevent path traversal attacks
|
|
26
|
+
if path.include?('..')
|
|
27
|
+
return [403, { 'Content-Type' => 'text/plain' }, ["Forbidden (Path Traversal)"]]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
file_path = File.join(@root, path)
|
|
31
|
+
|
|
32
|
+
if File.file?(file_path)
|
|
33
|
+
serve_file(file_path)
|
|
34
|
+
else
|
|
35
|
+
@app.call(env)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def serve_file(path)
|
|
42
|
+
ext = File.extname(path).downcase
|
|
43
|
+
content_type = MIME_TYPES[ext] || 'application/octet-stream'
|
|
44
|
+
|
|
45
|
+
headers = {
|
|
46
|
+
'Content-Type' => content_type,
|
|
47
|
+
'Cache-Control' => 'public, max-age=86400' # Cache for 1 day
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[200, headers, [File.read(path)]]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module EksCent
|
|
4
|
+
class MockRequest
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def get(uri, opts = {})
|
|
10
|
+
request('GET', uri, opts)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def post(uri, opts = {})
|
|
14
|
+
request('POST', uri, opts)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def request(method, uri, opts = {})
|
|
18
|
+
path, query = uri.split('?')
|
|
19
|
+
env = {
|
|
20
|
+
'REQUEST_METHOD' => method,
|
|
21
|
+
'PATH_INFO' => path,
|
|
22
|
+
'QUERY_STRING' => query || '',
|
|
23
|
+
'HTTP_USER_AGENT' => opts[:user_agent] || 'EksCent-MockRequest',
|
|
24
|
+
'rack.input' => StringIO.new(opts[:body] || '')
|
|
25
|
+
}.merge(opts[:env] || {})
|
|
26
|
+
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module EksCent
|
|
6
|
+
class Request
|
|
7
|
+
attr_reader :env
|
|
8
|
+
|
|
9
|
+
def initialize(env)
|
|
10
|
+
@env = env
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def request_method
|
|
14
|
+
@env['REQUEST_METHOD']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def path
|
|
18
|
+
@env['PATH_INFO'] || '/'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def query_string
|
|
22
|
+
@env['QUERY_STRING'] || ''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def params
|
|
26
|
+
@params ||= parse_params
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get?
|
|
30
|
+
request_method == 'GET'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def post?
|
|
34
|
+
request_method == 'POST'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user_agent
|
|
38
|
+
@env['HTTP_USER_AGENT']
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cookies
|
|
42
|
+
@cookies ||= parse_cookies
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def session
|
|
46
|
+
@env['eks_cent.session'] ||= {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse_cookies
|
|
52
|
+
cookie_header = @env['HTTP_COOKIE']
|
|
53
|
+
return {} unless cookie_header
|
|
54
|
+
|
|
55
|
+
CGI::Cookie.parse(cookie_header).transform_values(&:first)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_params
|
|
59
|
+
params = {}
|
|
60
|
+
|
|
61
|
+
# Parse query string
|
|
62
|
+
params.merge!(CGI.parse(query_string)) if query_string != ''
|
|
63
|
+
|
|
64
|
+
# Parse router params if any
|
|
65
|
+
if @env['eks_cent.router_params']
|
|
66
|
+
params.merge!(@env['eks_cent.router_params'])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Parse body based on content type
|
|
70
|
+
if @env['rack.input']
|
|
71
|
+
body = @env['rack.input'].read
|
|
72
|
+
@env['rack.input'].rewind if @env['rack.input'].respond_to?(:rewind)
|
|
73
|
+
|
|
74
|
+
if body && !body.empty?
|
|
75
|
+
if @env['CONTENT_TYPE'] == 'application/json'
|
|
76
|
+
begin
|
|
77
|
+
params.merge!(JSON.parse(body))
|
|
78
|
+
rescue JSON::ParserError
|
|
79
|
+
# Silently ignore invalid JSON or we could log it
|
|
80
|
+
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
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Flatten single values
|
|
89
|
+
params.transform_values { |v| v.is_a?(Array) && v.size == 1 ? v.first : v }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
module EksCent
|
|
5
|
+
class Response
|
|
6
|
+
attr_accessor :status, :headers, :body
|
|
7
|
+
|
|
8
|
+
def initialize(body = [], status = 200, headers = {})
|
|
9
|
+
@status = status
|
|
10
|
+
@headers = { 'Content-Type' => 'text/html' }.merge(headers)
|
|
11
|
+
@body = body.is_a?(String) ? [body] : body
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def write(str)
|
|
15
|
+
@body << str
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def redirect(target, status = 302)
|
|
19
|
+
@status = status
|
|
20
|
+
@headers['Location'] = target
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_cookie(name, value, options = {})
|
|
24
|
+
@cookies ||= {}
|
|
25
|
+
@cookies[name] = options.merge(value: value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render(template_name, locals = {})
|
|
29
|
+
require 'erb'
|
|
30
|
+
template_path = File.join('views', "#{template_name}.erb")
|
|
31
|
+
unless File.file?(template_path)
|
|
32
|
+
raise "Template tidak ditemukan: #{template_path}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
template_content = File.read(template_path)
|
|
36
|
+
|
|
37
|
+
# Gunakan context khusus agar helper h (escape HTML) tersedia
|
|
38
|
+
context = Object.new
|
|
39
|
+
context.extend(ERB::Util)
|
|
40
|
+
locals.each { |k, v| context.instance_variable_set("@#{k}", v) }
|
|
41
|
+
|
|
42
|
+
# Definisikan metode helper h secara eksplisit jika perlu
|
|
43
|
+
def context.h(s); html_escape(s); end
|
|
44
|
+
|
|
45
|
+
@body << ERB.new(template_content).result(context.instance_eval { binding })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def finish
|
|
49
|
+
if @cookies
|
|
50
|
+
@cookies.each do |name, opts|
|
|
51
|
+
cookie = "#{name}=#{CGI.escape(opts[:value].to_s)}"
|
|
52
|
+
cookie << "; expires=#{opts[:expires].httpdate}" if opts[:expires].is_a?(Time)
|
|
53
|
+
cookie << "; path=#{opts[:path]}" if opts[:path]
|
|
54
|
+
cookie << "; domain=#{opts[:domain]}" if opts[:domain]
|
|
55
|
+
cookie << "; secure" if opts[:secure]
|
|
56
|
+
cookie << "; httponly" if opts[:httponly]
|
|
57
|
+
cookie << "; samesite=#{opts[:samesite]}" if opts[:samesite]
|
|
58
|
+
|
|
59
|
+
if @headers['Set-Cookie']
|
|
60
|
+
@headers['Set-Cookie'] = [@headers['Set-Cookie'], cookie].flatten
|
|
61
|
+
else
|
|
62
|
+
@headers['Set-Cookie'] = cookie
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
[@status, @headers, @body]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.build
|
|
70
|
+
res = new
|
|
71
|
+
yield(res)
|
|
72
|
+
res.finish
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module EksCent
|
|
2
|
+
class Router
|
|
3
|
+
def initialize(&block)
|
|
4
|
+
@routes = {
|
|
5
|
+
'GET' => [],
|
|
6
|
+
'POST' => [],
|
|
7
|
+
'PUT' => [],
|
|
8
|
+
'PATCH' => [],
|
|
9
|
+
'DELETE' => []
|
|
10
|
+
}
|
|
11
|
+
@prefix = ''
|
|
12
|
+
@current_middlewares = []
|
|
13
|
+
instance_eval(&block) if block_given?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path, &block) add_route('GET', path, &block) end
|
|
17
|
+
def post(path, &block) add_route('POST', path, &block) end
|
|
18
|
+
def put(path, &block) add_route('PUT', path, &block) end
|
|
19
|
+
def patch(path, &block) add_route('PATCH', path, &block) end
|
|
20
|
+
def delete(path, &block) add_route('DELETE', path, &block) end
|
|
21
|
+
|
|
22
|
+
def namespace(prefix, &block)
|
|
23
|
+
old_prefix = @prefix
|
|
24
|
+
@prefix = "#{old_prefix}#{prefix}"
|
|
25
|
+
instance_eval(&block)
|
|
26
|
+
@prefix = old_prefix
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def group(middlewares: [], &block)
|
|
30
|
+
old_middlewares = @current_middlewares
|
|
31
|
+
@current_middlewares = old_middlewares + middlewares
|
|
32
|
+
instance_eval(&block)
|
|
33
|
+
@current_middlewares = old_middlewares
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(env)
|
|
37
|
+
req = Request.new(env)
|
|
38
|
+
|
|
39
|
+
route = find_route(req)
|
|
40
|
+
|
|
41
|
+
if route
|
|
42
|
+
# Extract params from path
|
|
43
|
+
match_data = route[:regex].match(req.path)
|
|
44
|
+
params = {}
|
|
45
|
+
route[:keys].each_with_index do |key, index|
|
|
46
|
+
params[key] = match_data[index + 1]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Inject params to env so they are available to all request objects
|
|
50
|
+
env['eks_cent.router_params'] = params
|
|
51
|
+
|
|
52
|
+
# Wrapped application with route-specific middlewares
|
|
53
|
+
app = proc do |e|
|
|
54
|
+
req_i = Request.new(e)
|
|
55
|
+
res_i = Response.new
|
|
56
|
+
instance_exec(req_i, res_i, &route[:block])
|
|
57
|
+
res_i.finish
|
|
58
|
+
end
|
|
59
|
+
route[:middlewares].reverse_each { |m| app = m.new(app) }
|
|
60
|
+
|
|
61
|
+
_status, headers, body = app.call(env)
|
|
62
|
+
[_status, headers, body]
|
|
63
|
+
else
|
|
64
|
+
[404, { 'Content-Type' => 'text/plain' }, ["Not Found"]]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def add_route(method, path, &block)
|
|
71
|
+
keys = []
|
|
72
|
+
full_path = "#{@prefix}#{path}".gsub('//', '/')
|
|
73
|
+
pattern = full_path.gsub(/:([\w\d_]+)/) do
|
|
74
|
+
keys << $1
|
|
75
|
+
"([^/]+)"
|
|
76
|
+
end
|
|
77
|
+
regex = /^#{pattern}$/
|
|
78
|
+
|
|
79
|
+
@routes[method] << {
|
|
80
|
+
path: full_path,
|
|
81
|
+
regex: regex,
|
|
82
|
+
keys: keys,
|
|
83
|
+
middlewares: @current_middlewares.dup,
|
|
84
|
+
block: block
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_route(req)
|
|
89
|
+
method_routes = @routes[req.request_method]
|
|
90
|
+
return nil unless method_routes
|
|
91
|
+
|
|
92
|
+
method_routes.find { |route| route[:regex].match?(req.path) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eks-cent
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- IshikawaUta
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: eksa-server
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.1'
|
|
26
|
+
description: Eks-Cent adalah framework web minimalis yang menyediakan sistem routing
|
|
27
|
+
canggih, manajemen session HMAC, dan proteksi keamanan bawaan menggunakan Eksa-Server
|
|
28
|
+
sebagai engine utama.
|
|
29
|
+
email:
|
|
30
|
+
- komikers09@gmail.com
|
|
31
|
+
executables:
|
|
32
|
+
- ekscentup
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- bin/ekscentup
|
|
39
|
+
- config.eks
|
|
40
|
+
- lib/eks-cent.rb
|
|
41
|
+
- lib/eks_cent/builder.rb
|
|
42
|
+
- lib/eks_cent/middleware/content_security.rb
|
|
43
|
+
- lib/eks_cent/middleware/logger.rb
|
|
44
|
+
- lib/eks_cent/middleware/session.rb
|
|
45
|
+
- lib/eks_cent/middleware/show_exceptions.rb
|
|
46
|
+
- lib/eks_cent/middleware/static.rb
|
|
47
|
+
- lib/eks_cent/mock_request.rb
|
|
48
|
+
- lib/eks_cent/request.rb
|
|
49
|
+
- lib/eks_cent/response.rb
|
|
50
|
+
- lib/eks_cent/router.rb
|
|
51
|
+
- lib/eks_cent/version.rb
|
|
52
|
+
homepage: https://github.com/IshikawaUta/eks-cent
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
allowed_push_host: https://rubygems.org
|
|
57
|
+
homepage_uri: https://github.com/IshikawaUta/eks-cent
|
|
58
|
+
source_code_uri: https://github.com/IshikawaUta/eks-cent
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 2.6.0
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.7
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Framework web Ruby ringan, aman, dan siap produksi berbasis Rack.
|
|
76
|
+
test_files: []
|