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 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
@@ -0,0 +1,3 @@
1
+ module EksCent
2
+ VERSION = '1.0.0'
3
+ 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: []