Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
by Lin Jen-Shin (godfat)
Pico web framework for building API-centric web applications. For Rack applications or Rack middleware. Around 250 lines of code.
Check jellyfish-contrib for extra extensions.
dup
in call
get %r{^/(?<id>\d+)$}
get '/'
get Matcher.new
Because Sinatra is too complex and inconsistent for me.
gem install jellyfish
You could also take a look at config.ru as an example.
require 'jellyfish'
class Tank
include Jellyfish
get '/' do
"Jelly Kelly\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
get %r{^/(?<id>\d+)$} do |match|
"Jelly ##{match[:id]}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
class Matcher
def match path
path.reverse == 'match/'
end
end
get Matcher.new do |match|
"#{match}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
post '/' do
headers 'X-Jellyfish-Life' => '100'
headers_merge 'X-Jellyfish-Mana' => '200'
body "Jellyfish 100/200\n"
status 201
'return is ignored if body has already been set'
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
get '/lookup' do
found "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
get '/crash' do
raise 'crash'
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
handle NameError do |e|
status 403
"No one hears you: #{e.backtrace.first}\n"
end
get '/yell' do
yell
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
handle Jellyfish::NotFound do |e|
status 404
"You found nothing."
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
handle Jellyfish::NotFound, NameError do |e|
status 404
"You found nothing."
end
get '/yell' do
yell
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
get '/report' do
"Your name is #{request.params['name']}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
get '/report' do
status, headers, body = jellyfish.call(env.merge('PATH_INFO' => '/info'))
self.status status
self.headers headers
self.body body
end
get('/info'){ "OK\n" }
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
Basically it's the same as defining a custom controller and then include the helper. This is merely a short hand. See next section for defining a custom controller.
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
temperature
end
module Helper
def temperature
"30\u{2103}\n"
end
end
controller_include Helper
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Heater.new
This is effectively the same as defining a helper module as above and include it, but more flexible and extensible.
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
temperature
end
class Controller < Jellyfish::Controller
def temperature
"30\u{2103}\n"
end
end
controller Controller
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Heater.new
We don't have before action built-in, but we could override dispatch
in
the controller to do the same thing. CAVEAT: Remember to call super
.
require 'jellyfish'
class Tank
include Jellyfish
controller_include Module.new{
def dispatch
@state = 'jumps'
super
end
}
get do
"Jelly #{@state}.\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
Default Rack::Builder
and Rack::URLMap
is routing via linear search,
which could be very slow with a large number of routes. We could use
Jellyfish::Builder
in this case because it would compile the routes
into a regular expression, it would be matching much faster than
linear search.
Note that Jellyfish::Builder
is not a complete compatible implementation.
The followings are intentional:
There's no Jellyfish::Builder.call
because it doesn't make sense in my
opinion. Always use Jellyfish::Builder.app
instead.
There's no Jellyfish::Builder.parse_file
and
Jellyfish::Builder.new_from_string
because Rack servers are not
going to use Jellyfish::Builder
to parse config.ru
at this point.
We could provide this if there's a need.
Jellyfish::URLMap
does not modify env
, and it would call the app with
another instance of Hash. Mutating data is a bad idea.
All other tests passed the same test suites for Rack::Builder
and
Jellyfish::URLMap
.
require 'jellyfish'
run Jellyfish::Builder.app{
map '/a' do; run lambda{ |_| [200, {}, ["a\n"] ] }; end
map '/b' do; run lambda{ |_| [200, {}, ["b\n"] ] }; end
map '/c' do; run lambda{ |_| [200, {}, ["c\n"] ] }; end
map '/d' do; run lambda{ |_| [200, {}, ["d\n"] ] }; end
map '/e' do
map '/f' do; run lambda{ |_| [200, {}, ["e/f\n"]] }; end
map '/g' do; run lambda{ |_| [200, {}, ["e/g\n"]] }; end
map '/h' do; run lambda{ |_| [200, {}, ["e/h\n"]] }; end
map '/i' do; run lambda{ |_| [200, {}, ["e/i\n"]] }; end
map '/' do; run lambda{ |_| [200, {}, ["e\n"]] }; end
end
map '/j' do; run lambda{ |_| [200, {}, ["j\n"] ] }; end
map '/k' do; run lambda{ |_| [200, {}, ["k\n"] ] }; end
map '/l' do; run lambda{ |_| [200, {}, ["l\n"] ] }; end
map '/m' do
map '/g' do; run lambda{ |_| [200, {}, ["m/g\n"]] }; end
run lambda{ |_| [200, {}, ["m\n"] ] }
end
use Rack::ContentLength
run lambda{ |_| [200, {}, ["/\n"]] }
}
You could try a stupid benchmark yourself:
ruby -Ilib bench/bench_builder.rb
For a 1000 routes app, here's my result:
Calculating -------------------------------------
Jellyfish::URLMap 5.726k i/100ms
Rack::URLMap 167.000 i/100ms
-------------------------------------------------
Jellyfish::URLMap 62.397k (± 1.2%) i/s - 314.930k
Rack::URLMap 1.702k (± 1.5%) i/s - 8.517k
Comparison:
Jellyfish::URLMap: 62397.3 i/s
Rack::URLMap: 1702.0 i/s - 36.66x slower
listen
is a convenient way to define routing based on the host. We could
also use map
inside listen
block. Here's a quick example that specifically
listen on a particular host for long-polling and all other hosts would go to
the default app.
require 'jellyfish'
long_poll = lambda{ |env| [200, {}, ["long_poll #{env['HTTP_HOST']}\n"]] }
fast_app = lambda{ |env| [200, {}, ["fast_app #{env['HTTP_HOST']}\n"]] }
run Jellyfish::Builder.app{
listen 'slow-app' do
run long_poll
end
run fast_app
}
map path, host:
)Alternatively, we could pass host
as an argument to map
so that the
endpoint would only listen on a specific host.
require 'jellyfish'
long_poll = lambda{ |env| [200, {}, ["long_poll #{env['HTTP_HOST']}\n"]] }
fast_app = lambda{ |env| [200, {}, ["fast_app #{env['HTTP_HOST']}\n"]] }
run Jellyfish::Builder.app{
map '/', host: 'slow-app' do
run long_poll
end
run fast_app
}
map "http://#{path}"
)Or if you really prefer the Rack::URLMap
compatible way, then you could
just add http://host
to your path prefix. https
works, too.
require 'jellyfish'
long_poll = lambda{ |env| [200, {}, ["long_poll #{env['HTTP_HOST']}\n"]] }
fast_app = lambda{ |env| [200, {}, ["fast_app #{env['HTTP_HOST']}\n"]] }
run Jellyfish::Builder.app{
map 'http://slow-app' do
run long_poll
end
run fast_app
}
Jellyfish::Builder
is mostly compatible with Rack::Builder
, and
Jellyfish::Rewrite
is an extension to Rack::Builder
which allows
you to rewrite env['PATH_INFO']
in an easy way. In an ideal world,
we don't really need this. But in real world, we might want to have some
backward compatible API which continues to work even if the API endpoint
has already been changed.
Suppose the old API was: /users/me
, and we want to change to /profiles/me
,
while leaving the /users/list
as before. We may have this:
require 'jellyfish'
users_api = lambda{ |env| [200, {}, ["/users#{env['PATH_INFO']}\n"]] }
profiles_api = lambda{ |env| [200, {}, ["/profiles#{env['PATH_INFO']}\n"]] }
run Jellyfish::Builder.app{
rewrite '/users/me' => '/me' do
run profiles_api
end
map '/profiles' do
run profiles_api
end
map '/users' do
run users_api
end
}
This way, we would rewrite /users/me
to /profiles/me
and serve it with
our profiles API app, while leaving all other paths begin with /users
continue to work with the old users API app.
map path, to:
)Note that you could also use map path, :to
if you prefer this API more:
require 'jellyfish'
users_api = lambda{ |env| [200, {}, ["/users#{env['PATH_INFO']}\n"]] }
profiles_api = lambda{ |env| [200, {}, ["/profiles#{env['PATH_INFO']}\n"]] }
run Jellyfish::Builder.app{
map '/users/me', to: '/me' do
run profiles_api
end
map '/profiles' do
run profiles_api
end
map '/users' do
run users_api
end
}
rewrite rules
)Note that rewrite
takes a hash which could contain more than one rule:
require 'jellyfish'
profiles_api = lambda{ |env| [200, {}, ["/profiles#{env['PATH_INFO']}\n"]] }
run Jellyfish::Builder.app{
rewrite '/users/me' => '/me',
'/users/fa' => '/fa' do
run profiles_api
end
}
require 'jellyfish'
class Tank
include Jellyfish
controller_include Jellyfish::NormalizedParams
get %r{^/(?<id>\d+)$} do
"Jelly ##{params[:id]}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
require 'jellyfish'
class Tank
include Jellyfish
controller_include Jellyfish::NormalizedPath
get "/\u{56e7}" do
"#{env['PATH_INFO']}=#{path_info}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
Note that the controller should be assigned lastly in order to include modules remembered in controller_include.
require 'jellyfish'
class Tank
include Jellyfish
class MyController < Jellyfish::Controller
include Jellyfish::WebSocket
end
controller_include NormalizedParams, NormalizedPath
controller MyController
get %r{^/(?<id>\d+)$} do
"Jelly ##{params[:id]} jumps.\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new
If the Jellyfish middleware cannot find a corresponding action, it would
then forward the request to the lower application. We call this cascade
.
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
"30\u{2103}\n"
end
end
class Tank
include Jellyfish
get '/' do
"Jelly Kelly\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new
We could also explicitly call the lower app. This would give us more flexibility than simply forwarding it.
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
status, headers, body = jellyfish.app.call(env)
self.status status
self.headers headers
self.body body
headers_merge('X-Temperature' => "30\u{2103}")
end
end
class Tank
include Jellyfish
get '/status' do
"See header X-Temperature\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new
We could also override cascade
in order to craft custom response when
forwarding is happening. Note that whenever this forwarding is happening,
Jellyfish won't try to merge the headers from dispatch
method, because
in this case Jellyfish is served as a pure proxy. As result we need to
explicitly merge the headers if we really want.
require 'jellyfish'
class Heater
include Jellyfish
controller_include Module.new{
def dispatch
headers_merge('X-Temperature' => "35\u{2103}")
super
end
def cascade
status, headers, body = jellyfish.app.call(env)
halt [status, headers_merge(headers), body]
end
}
end
class Tank
include Jellyfish
get '/status' do
"\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
env['temperature'] = 30
cascade
end
end
class Tank
include Jellyfish
get '/status' do
"#{env['temperature']}\u{2103}\n"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new
require 'jellyfish'
class Heater
include Jellyfish
get '/status' do
"30\u{2103}\n"
end
end
class Tank
include Jellyfish
get '/' do
"Jelly Kelly\n"
end
end
HugeTank = Rack::Builder.app do
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new
end
run HugeTank
require 'jellyfish'
class Protector
include Jellyfish
handle StandardError do |e|
"Protected: #{e}\n"
end
end
class Tank
include Jellyfish
handle_exceptions false # default is true, setting false here would make
# the outside Protector handle the exception
get '/' do
raise "Oops, tank broken"
end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Protector
run Tank.new
You would need a proper server setup. Here's an example with Rainbows and fibers:
class Tank
include Jellyfish
get '/chunked' do
ChunkedBody.new{ |out|
(0..4).each{ |i| out.call("#{i}\n") }
}
end
end
use Rack::ContentType, 'text/plain'
run Tank.new
class Tank
include Jellyfish
class Body
def each
(0..4).each{ |i| yield "#{i}\n" }
end
end
get '/chunked' do
Body.new
end
end
use Rack::ContentType, 'text/plain'
run Tank.new
class Tank
include Jellyfish
class Body
def each
(0..4).each{ |i| yield "data: #{i}\n\n" }
end
end
get '/sse' do
headers_merge('content-type' => 'text/event-stream')
Body.new
end
end
run Tank.new
class Tank
include Jellyfish
get '/sse' do
headers_merge(
'content-type' => 'text/event-stream',
'rack.hijack' => lambda do |sock|
(0..4).each do |i|
sock.write("data: #{i}\n\n")
end
sock.close
end)
end
end
run Tank.new
Note that this only works for Rack servers which support hijack. You're better off with a threaded server such as Rainbows! with thread based concurrency model, or Puma.
Event-driven based server is a whole different story though. Since EventMachine is basically dead, we could see if there would be a Celluloid-IO based web server production ready in the future, so that we could take the advantage of event based approach.
class Tank
include Jellyfish
controller_include Jellyfish::WebSocket
get '/echo' do
switch_protocol do |msg|
ws_write(msg)
end
ws_write('Hi!')
ws_start
end
end
run Tank.new
Apache License 2.0 (Apache-2.0)
Copyright (c) 2012-2023, Lin Jen-Shin (godfat)
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
FAQs
Unknown package
We found that jellyfish demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.