a simple web proxy using WEBrick
As the README states, “WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server”. I recently needed to create a small proxy, with minimal effort, in order to hide some functionality of a proof-of-concept API. WEBrick is quite powerful, and a small proxy does not require more than 20-25 lines or code.
The AbstractServlet
class allows us to respond to GET
, HEAD
and OPTIONS
requests. We can use it to encapsulate our
logic of receiving a request, forwarding it to the server (possibly changing it) and responding back.
Let’s write a simple proxy, that receives a request, appends a custom query param and forwards it to
the API server https://api-server.com
. We will append the realm
query parameter with a hardcoded
value of qa-realm
.
First, let’s create our proxy as a subclass of AbstractServlet
. We will implement the
do_GET method as we care only
for GET
requests. The incoming request object is stored in the request
parameter, we will manipulate
this later in order to add our query parameter. In order to respond, we need to set the content_type
and
body values on the response
object.
require "webrick"
class MyProxy < WEBrick::HTTPServlet::AbstractServlet
def do_GET(request, response)
response.content_type = "text/plain"
response.body = "It works!"
end
end
Next, we will add the realm parameter. We need to parse the request URL in order to make sure that we handle correctly all use cases involving any existing parameters. We will use URI for this.
require "webrick"
require "uri"
class MyProxy < WEBrick::HTTPServlet::AbstractServlet
REALM = "qa-realm"
def do_GET(request, response)
uri = forwarded_uri(request.unparsed_uri)
response.content_type = "text/plain"
response.body = "It works! New URI is #{uri}"
end
private
def forwarded_uri(unparsed_uri)
uri = URI(unparsed_uri)
params = URI.decode_www_form(uri.query || "") << ["realm", REALM]
uri.query = URI.encode_www_form(params)
uri.to_s
end
end
Right now, we manipulate the request URI, but we don’t forward anything to the intended endpoint. We will use Net::HTTP to forward the request and pass the response back. We will also use the body, and the content type we retrieve from the proxied server when we pass the response back.
require "webrick"
require "net/http"
require "uri"
class MyProxy < WEBrick::HTTPServlet::AbstractServlet
HOST = "api-server.com"
REALM = "qa-realm"
def do_GET(request, response)
uri = forwarded_uri(request.unparsed_uri)
http = Net::HTTP.new(HOST, 443)
http.use_ssl = true
resp = http.request(Net::HTTP::Get.new(uri))
body = resp.body
response.content_type = resp["content-type"]
response.body = body
end
private
def forwarded_uri(unparsed_uri)
uri = URI(unparsed_uri)
params = URI.decode_www_form(uri.query || "") << ["realm", REALM]
uri.query = URI.encode_www_form(params)
uri.to_s
end
end
In order to run our proxy, we need a few more missing pieces. First we need to create a new WEBrick::HTTPServer
server = WEBrick::HTTPServer.new(:Port => ENV["PORT"] || 8080)
Then we need to mount our proxy under an endpoint, we can use the root endpoint or any other we want.
server.mount "/", MyProxy
Finally, let’s allow stopping the server using Ctrl+C and then start the server.
trap("INT"){ server.shutdown }
server.start
If we run our proxy with ruby myproxy.rb
it will start serving using port 8080 under /.
[2021-01-24 21:36:00] INFO WEBrick 1.6.0
[2021-01-24 21:36:00] INFO ruby 2.7.1 (2020-03-31) [x86_64-darwin18]
[2021-01-24 21:36:00] INFO WEBrick::HTTPServer#start: pid=47104 port=8080
The final code is the following:
#!/usr/bin/env ruby
# frozen_string_literal: true
require "webrick"
require "net/http"
require "uri"
class MyProxy < WEBrick::HTTPServlet::AbstractServlet
HOST = "api-server.com"
REALM = "qa-realm"
def do_GET(request, response)
uri = forwarded_uri(request.unparsed_uri)
http = Net::HTTP.new(HOST, 443)
http.use_ssl = true
resp = http.request(Net::HTTP::Get.new(uri))
body = resp.body
response.content_type = resp["content-type"]
response.body = body
end
private
def forwarded_uri(unparsed_uri)
uri = URI(unparsed_uri)
params = URI.decode_www_form(uri.query || "") << ["realm", REALM]
uri.query = URI.encode_www_form(params)
uri.to_s
end
end
server = WEBrick::HTTPServer.new(:Port => ENV["PORT"] || 8080)
server.mount "/", MyProxy
trap("INT"){ server.shutdown }
server.start