I am a newer of elixir。I
need to forward the websocket request from the cowboy sever http://127.0.0.1:4000/api to another server http://127.0.0.1:8080/abc. In short, how to implement reverse proxy websockets using Cowboy?
The following is the framework I used:
cowboy: 2.9.0
plug_cowboy: 2.6.0
plug: 1.14.0
The following is my simple code:
# application.ex
defmodule Example.Application do
use Application
require Logger
def start(_type, _args) do
children = [
{Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: 4000]}
]
opts = [strategy: :one_for_one, name: Example.Supervisor]
Logger.info("Starting application...")
Supervisor.start_link(children, opts)
end
end
# router.ex
defmodule Example.Router do
use Plug.Router
plug :match
plug :dispatch
get "/" do
send_resp(conn, 200, "Welcome")
end
get "/start" do
send_resp(conn, 200, "here is start page!")
end
get "/hello" do
send_resp(conn, 200, "hello world!")
end
get "/api" do
send_resp(conn, 200, "need to upforward websocket request to http://127.0.0.1:8080")
end
match _ do
send_resp(conn, 404, "Oops!")
end
end
The resposity of the project is https://github.com/hrzyang/elixir_plug_cowboy_example
I have read the plug_cowboy document of WebSocket support, but I just don't understand the meaning. Read it or may bring some help with you !
Related
I am trying to write a simple Slack chatbot for my team using Ruby. It's a bit rough because Slack doesn't have official support for Ruby. Nevertheless, I've been able to open a websocket and listen to Slack events using this code I wrote:
# frozen_string_literal: true
require "async"
require "async/io/stream"
require "async/http/endpoint"
require "async/websocket/client"
require "excon"
require "json"
module Slack
class Client
Error = Class.new(StandardError)
AquisitionError = Class.new(Error)
ConnectionError = Class.new(Error)
CONNECTION_AQUISITION_ENDPOINT = "https://slack.com/api/apps.connections.open"
def initialize
#token = "my-app-token"
end
def connect
connection_info = Excon.post(CONNECTION_AQUISITION_ENDPOINT, headers: {
"Content-type": "application/x-www-form-urlencoded",
Authorization: "Bearer #{#token}",
})
result = JSON.parse(connection_info.body)
raise(AquisitionError) unless result["ok"] # better error later
websocket = Async::HTTP::Endpoint.parse(result["url"])
Async do |_task|
Async::WebSocket::Client.connect(websocket) do |connection|
payload = connection.read
raise(ConnectionError) unless connection_check(payload)
puts "Listening..."
handle(payload) while (payload = connection.read)
end
end
end
private
def connection_check(payload)
payload[:type] == "hello"
end
def handle(payload)
puts payload.inspect
end
end
end
The documentation leads me to believe that I can write JSON to this connection e.g.
connection.write({
# JSON to send a message in Slack here
# Probably need to specify the channel somehow
# Probably need to specify if I'm using markdown
# Have no idea what the form should be
})
But I haven't been able to figure out what form that JSON needs to take.
I was also running into this and trying to determine why the docs do not provide a clear answer. It appears that you do not infact send requests back over websockets, and instead use websockets only to accept requests.
Which would mean that you would use something like the webAPI to perform responsive actions, you can see this pattern in the PythonSDK with websockets example provided by Slack.
eg:
if req.type == "events_api":
# Acknowledge the request, this ack is sent back over websockets
# the format is: { 'envelope_id': req['envelope_id'] }
response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response)
# Add a reaction to the message if it's a new message
# notice that this request uses the web_client here and not the socket one
if req.payload["event"]["type"] == "message" \
and req.payload["event"].get("subtype") is None:
client.web_client.reactions_add(
name="eyes",
channel=req.payload["event"]["channel"],
timestamp=req.payload["event"]["ts"],
)
I'm trying to use the Ruby gem http-2 to send a GET request to Google.
I've lifted the code directly from the example and simplified it slightly:
require 'http/2'
require 'socket'
require 'openssl'
require 'uri'
uri = URI.parse('http://www.google.com/')
tcp = TCPSocket.new(uri.host, uri.port)
sock = tcp
conn = HTTP2::Client.new
conn.on(:frame) do |bytes|
# puts "Sending bytes: #{bytes.unpack("H*").first}"
sock.print bytes
sock.flush
end
conn.on(:frame_sent) do |frame|
puts "Sent frame: #{frame.inspect}"
end
conn.on(:frame_received) do |frame|
puts "Received frame: #{frame.inspect}"
end
stream = conn.new_stream
stream.on(:close) do
puts 'stream closed'
sock.close
end
stream.on(:half_close) do
puts 'closing client-end of the stream'
end
stream.on(:headers) do |h|
puts "response headers: #{h}"
end
stream.on(:data) do |d|
puts "response data chunk: <<#{d}>>"
end
head = {
':scheme' => uri.scheme,
':method' => 'GET',
':path' => uri.path
}
puts 'Sending HTTP 2.0 request'
stream.headers(head, end_stream: true)
while !sock.closed? && !sock.eof?
data = sock.read_nonblock(1024)
# puts "Received bytes: #{data.unpack("H*").first}"
begin
conn << data
rescue => e
puts "#{e.class} exception: #{e.message} - closing socket."
e.backtrace.each { |l| puts "\t" + l }
sock.close
end
end
The output is:
Sending HTTP 2.0 request
Sent frame: {:type=>:settings, :stream=>0, :payload=>[[:settings_max_concurrent_streams, 100]]}
Sent frame: {:type=>:headers, :flags=>[:end_headers, :end_stream], :payload=>[[":scheme", "http"], [":method", "GET"], [":path", "/"]], :stream=>1}
closing client-end of the stream
(Note: you get pretty much the same output as above by running the actual example file, i.e., ruby client.rb http://www.google.com/)
Why is no response data being displayed?
Public servers like google.com do not support HTTP/2 in clear text.
You are trying to connect to http://google.com, while you should really connect to https://google.com (note the https scheme).
In order to do that, you may need to wrap the TCP socket using TLS (see for example here), if http-2 does not do it for you.
Note also that HTTP/2 requires strong TLS ciphers and ALPN, so make sure that you have an updated version of OpenSSL (at least 1.0.2).
Given that the author of http-2 is a strong HTTP/2 supporter, I am guessing that your only problem is the fact that you tried clear-text http rather than https, and I expect that TLS cipher strength and ALPN are taken care of by the http-2 library.
Hi I have a need to be able to receive requests from GitLab (body => JSON) as well as serve files on same port. I am trying to use Webrick for this purpose. I can do these separately.
To serve files I do:
server = WEBrick::HTTPServer.new(:Port => 3030, :DocumentRoot => '/')
server.start
To receive and process jSON I do:
server = WEBrick::HTTPServer.new(:Port => 3030, :DocumentRoot => '/')
server.mount_proc '/' do | req, res |
Queue.new(req.body)
end
But I need this functionality combined, is there a way to do this with Webrick?
Yes, this is certainly possible with Webrick or any HTTP server. There will be two different HTTP actions depending on what the users wants to do, 1.) a GET request to serve the files or 2.) a POST request to process some JSON.
Here's a simple example to show you how to do both:
class Server < WEBrick::HTTPServlet::AbstractServlet
def do_GET (request, response)
puts "this is a get request"
end
def do_POST (request, response)
puts "this is a post request who received #{request.body}"
end
end
server = WEBrick::HTTPServer.new(:Port => 3030)
server.mount "/", Server
trap("INT") {
server.shutdown
}
server.start
Once that is running you can test this by doing the following in a separate terminal window:
curl localhost:3030
output:
this is a get request
localhost - - [23/Apr/2015:06:39:20 EDT] "GET / HTTP/1.1" 200 0
- -> /
To test the POST request:
curl -d "{\"json\":\"payload\"}" localhost:3030
output:
this is a post request who received {"json":"payload"}
localhost - - [23/Apr/2015:06:40:07 EDT] "POST / HTTP/1.1" 200 0
- -> /
Since you mentioned the purpose was a light code-base, here's a light, fast script using the Plezi framework...
This will allow for easier testing, I think (but I'm biased). Also, Plezi is faster on my machine then Webrick (although it's a pure ruby framework, no rack or 'c' extensions involved).
require 'plezi'
class MyController
def index
# parsed JSON is acceible via the params Hash i.e. params[:foo]
# raw JSON request is acceible via request[:body]
# returned response can be set by returning a string...
"The request's params (parsed):\n#{params}\n\nThe raw body:\n#{request[:body]}"
end
end
# start to listen and set the root path for serving files.
listen root: './'
# set a catch-all route so that MyController#index is always called.
route '*', MyController
(if you're running the script from the terminal, remember to exit irb using the exit command - this will activate the web server)
I have Savon working in a Sinatra ruby application. The application will be called frequently, and I don't want to lean on the server too much.
It looks to me that everytime the /test_savon GET is hit, I am going to the server and asking for the wdsl again. I would only need to do that once, it would seem.
Should I make a few clients as ruby globals (one for each wsdl) and use them repeatedly?
Here is my code which works: NTLM auth - talking to a MS DynamicsNav Server
get '/test_savon' do
# create a client for the service
client = Savon.client(wsdl: 'http://somedynamicsnavserver:7047/WS/Page/Salesperson', ntlm: ["username", "password"]) do
convert_request_keys_to :camelcase
end
operations = client.operations
puts "operations are #{operations.to_s}" if operations
puts "checked operations" if operations
# => [:find_user, :list_users]
# call the 'findUser' operation
response = client.call(:read, message: { code: 'salepersonIDhere' })
puts "response is #{response.to_s}" if response
response.body.to_s
# => {:read_result=>{:salesperson=>{:key=>"aKey", :code=>"salepersonIDhere", :name=>"Jim Kirk", :global_code=>"X", :phone_no=>"4407"}, :#xmlns=>"urn:microsoft-dynamics-schemas/page/salesperson"}}
end
I usually don't use a WSDL at all but work without it. That should be much faster because you should have less roundtrips.
A small example:
#!ruby
gem "savon", "~>2.0"
require 'savon'
stock_handle = ARGV[0] || 'OTEX'
client = Savon.client(
endpoint: 'http://www.webservicex.net/stockquote.asmx',
namespace: 'http://www.webserviceX.NET/',
convert_request_keys_to: :camelcase, # :camelcase, :upcase, :none
log: true,
log_level: :debug,
pretty_print_xml: true
)
response = client.call(
:get_quote,
soap_action: 'http://www.webserviceX.NET/GetQuote',
message: { "wsdl:symbol" => stock_handle}
)
print response.to_hash
I'm trying to write a simple zmq system to replace an http client/server. My client times out when the server is off/unavailable, but does not retry or stop. What am I missing?
zmq_client.rb (modified version of Han Holl's lazy pirate client from zeromq guide)
require 'rubygems'
require 'zmq'
context = ZMQ::Context.new
socket = context.socket(ZMQ::REQ)
socket.connect('tcp://localhost:5559')
retries = 2
timeout = 10
retries.times do |tries|
message = "Hello #{tries}"
raise("Send: #{message} failed") unless socket.send(message)
puts "Sending string [#{message}]"
if ZMQ.select( [socket], nil, nil, timeout)
message = socket.recv
puts "Received reply [#{message}]"
break
else
puts "timeout"
end
end
socket.close
zmq_broker.rb (modified version of Oleg Sidorov's code found on zeromq guide)
require 'rubygems'
require 'ffi-rzmq'
context = ZMQ::Context.new
frontend = context.socket(ZMQ::ROUTER)
frontend.bind('tcp://*:5559')
poller = ZMQ::Poller.new
poller.register(frontend, ZMQ::POLLIN)
loop do
poller.poll(:blocking)
poller.readables.each do |socket|
if socket === frontend
loop do
socket.recv_string(message = '')
more = socket.more_parts?
puts "#{message}#{more}"
socket.send_string(message, more ? ZMQ::SNDMORE : 0)
break unless more
end
end
end
end
You should get an error Send: #{message} failed as soon as you try to send again after the first timeout, because your 2nd send will happen directly after the 1st send, and the REQ socket enforces that each send must go after (successful, not timeout-ed) recv.
In the lazy pirate pattern, you may need to send several requests before getting a reply. Solution suggested in the 0MQ Guide is to close and reopen the REQ socket after an error. Your client doesn't close/reopen the REQ socket.
You may find helpful the "Lazy Pirate client in Ruby" example from the Guide.