Add packages/ruby/server/ (#132)

### Summary

The `server` package implements a static file web server intended to be 
used during development. The goal is to have it as close to a production
environment as possible. Not there yet. This is the first step.
This commit is contained in:
Robert 2023-06-25 21:03:07 -03:00 committed by GitHub
parent 564b01da62
commit bf1558eeb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 209 additions and 104 deletions

View file

@ -2,9 +2,6 @@
require "ryo"
require "listen"
load "tasks/config/build.rake"
load "tasks/config/install.rake"
load "tasks/deploy.rake"
namespace :nanoc do
@ -58,5 +55,5 @@ namespace :lint do
end
end
end
task lint: ["linter:ruby", "linter:typescript"]
task lint: ["lint:rubocop", "lint:eslint"]
task default: "deploy:local"

8
packages/ruby/server/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
build/
tmp/
node_modules/
.bundle/
*.log
*.sh
*.core
Gemfile.lock

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
source "https://rubygems.org"
gemspec

View file

@ -0,0 +1,15 @@
Copyright (C) 2023 by 0x1eef <0x1eef@protonmail.com>
Permission to use, copy, modify, and/or distribute this
software for any purpose with or without fee is hereby
granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS
ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
OF THIS SOFTWARE.

View file

@ -0,0 +1,40 @@
## About
server.rb implements a static file web server
by using the fast performing Ruby web server
[Puma](https://github.com/puma/puma)
and a small
[Rack](https://github.com/rack/rack)
application.
## Examples
### Server.for_dir
The `Server.for_dir` method returns a Server instance
that serves the contents of a directory. `Server#start` spawns
a new thread to listen for requests, and afterwards returns
the thread. `Thread#join` can block execution at that point,
or execution can continue as normal by not calling `Thread#join`:
```ruby
require "server"
##
# Create a Server instance for the contents of a directory
server = Server.for_dir("./build/website/")
##
# Start listening for connections
thr = server.start
##
# Prevent the main thread from exiting
thr.join
```
## License
[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/).
<br>
See [LICENSE](./LICENSE).

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
class Server
require "rack"
require_relative "server/puma"
require_relative "server/dir"
def self.app(path)
Rack::Builder.app do
run Server::Dir.new(path)
end
end
def self.for_dir(path, options = {})
new(app(path), options)
end
def initialize(app, options = {})
@app = app
@options = default_options.merge!(options)
@events = Puma::Events.new
@server = Puma::Server.new(@app, @events, @options)
end
def start
@server.binder.parse(@options[:binds])
@server.run
end
def stop
@server.stop
end
private
def default_options
{
tcp_host: "127.0.0.1",
tcp_port: 7777,
binds: ["tcp://127.0.0.1:7777"],
supported_http_methods: %w[GET HEAD],
min_threads: 1,
max_threads: 5,
workers: 1
}
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
class Server::Dir
def initialize(root)
@root = File.realpath(root)
end
def call(env)
req = Rack::Request.new(env)
headers = Rack::Headers.new(env)
h, body = read(local_path(req.path))
[200, headers.merge!(h), body]
rescue Errno::ENOENT
not_found
end
private
attr_reader :root
def read(path)
body = File.binread(path)
[{"Content-Length" => body.bytesize}, body.each_line]
end
def local_path(req_path)
lpath = File.join root, File.expand_path(req_path)
File.directory?(lpath) ? File.join(lpath, "index.html") : lpath
end
def not_found
[404, {"Content-Type" => "text/plain"}, ["The requested URL was not found"]]
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
require "puma"
require "puma/events"
class Puma::Server
public :binder
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
Gem::Specification.new do |gem|
gem.name = "server.rb"
gem.authors = ["0x1eef"]
gem.email = ["0x1eef@protonmail.com"]
gem.homepage = "https://github.com/0x1eef/server.rb#readme"
gem.version = "0.1.0"
gem.licenses = ["0BSD"]
gem.files = Dir["lib/*", "lib/**/*.rb"]
gem.require_paths = ["lib"]
gem.summary = "A static file web server"
gem.description = gem.summary
gem.add_runtime_dependency "puma", "~> 6.3"
gem.add_runtime_dependency "rack", "~> 3.0"
gem.add_development_dependency "standard", "~> 1.24"
gem.add_development_dependency "rack-test", "~> 2.1"
gem.add_development_dependency "test-unit", "~> 3.5"
end

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<head>
<title>/index.html</title>
</head>
<body>
<b>/index.html</b>
</body>
</html>

View file

@ -0,0 +1,23 @@
require_relative "setup"
require "rack/test"
class ServerDirTest < Test::Unit::TestCase
include Rack::Test::Methods
def test_index
get "/"
assert_equal 200, last_response.status
assert_equal "text/html", last_response.content_type
assert_equal bytesize("./test/fakeweb/index.html"), last_response.content_length
end
private
def app
@app ||= Server.app("./test/fakeweb/")
end
def bytesize(path)
File.binread(path).bytesize
end
end

View file

@ -0,0 +1,3 @@
require "bundler/setup"
require "test/unit"
require "server"

View file

@ -1,21 +0,0 @@
##
# frozen_string_literal: true
module PF
def pf_in(rule)
[
rule.proto && "proto #{rule.proto}",
"from #{rule.from}",
"to #{rule.to}",
rule.port && "port #{rule.port}"
].compact.join(" ")
end
def pf_out(rule)
[
rule.proto && "proto #{rule.proto}",
"to #{rule.to}",
rule.port && "port #{rule.port}"
].compact.join(" ")
end
end

View file

@ -1,69 +0,0 @@
##
# frozen_string_literal: true
require "bundler/setup"
require "erb"
require "ryo"
require "yaml"
require_relative "../../tasks.lib/erb_context"
read_options = ->(env:) do
path = File.join(Dir.getwd, "config", "#{env}.yml")
Ryo.from(YAML.load_file(path))
end
get_build_dir = -> (env:) do
path = File.join("./build", env)
mkdir_p(path) unless Dir.exist?(path)
path
end
build_files = -> (env:, base:, glob:) do
options = read_options.call(env:)
build_dir = get_build_dir.call(env:)
context = ERBContext.with_locals(options)
Dir.glob(glob, base:).each do |file|
erbf = File.join(base, file)
path = File.join(build_dir, File.dirname(file))
dest = File.join(path, File.basename(file, ".erb"))
mkdir_p(path)
File.binwrite dest,
ERB.new(File.binread(erbf), trim_mode: "-").result(context)
print "View #{dest} [y/n]:"
system("cat #{dest} | less") if $stdin.gets.chomp == "y"
end
end
desc "Build configuration files"
task "config:build", :env do |task, args|
env = args[:env]
case env
when "remote"
Rake::Task["config:build:etc"].invoke(env)
when "local"
# no-op
else
warn "env should be 'remote', or 'local', got: #{env}"
end
Rake::Task["config:build:nginx"].invoke(env)
end
desc "Build /etc configuration files"
task "config:build:etc", :env do |task, args|
env = args[:env]
build_files.call(
env:,
base: "config/#{env}",
glob: "etc/*.conf.erb"
)
end
desc "Build nginx configuration files"
task "config:build:nginx", :env do |task, args|
env = args[:env]
build_files.call(
env:,
base: "config/generic",
glob: "usr.local.etc/**/*.conf.erb"
)
end

View file

@ -1,10 +0,0 @@
desc "Install configuration files"
task "config:install", :env do |tasks, args|
env = args[:env]
if Process.euid != 0
sh "doas -u root bundle exec rake config:install[#{env}]"
exit $?.exitstatus
end
copy_entry File.join(Dir.getwd, "build", env, "usr.local.etc"),
"/usr/local/etc"
end