Multiple Endpoints (for replicated SOAP Servers) with SAVON - ruby-on-rails

I am trying to make a SOAP request to a remote SOAP Server using the Savon gem in rails. The SOAP Server is replicated on three instances with different IP Addresses, i.e. XXX.XXX.XXX.1, XXX.XXX.XXX.2 and XXX.XXX.XXX.3.
I am able to explicitly define the savon client endpoint using one of the above ip addresses like so:
Savon.client(
endpoint: "http://XXX.XXX.XXX.1:15043/enmac/SOAP",
namespace: '',
convert_request_keys_to: :camelcase,
env_namespace: 'SOAP-ENV',
namespace_identifier: nil,
log: true,
log_level: :info,
pretty_print_xml: true,
read_timeout: 90,
open_timeout: 90,
headers: {
"Accept-Encoding" => "gzip"
}
)
The problem with the above sample code is that when the SOAP Server instance with IP Address XXX.XXX.XXX.1 is down then my services goes down as well.
What I am actually looking for is an approach that will make sure that when SOAP Server instance XXX.XXX.XXX.1 is down then XXX.XXX.XXX.2 takes over etc.
Below is what I tried but couldn't get it to work properly:
I have a database table with all the SOAP Servers instances populated by IP address with only a single of them with a status of active
I have a the following method that shift to then next server:
def shift_oms_server
#oms_server = OmsServer.where(status: true).first
case #oms_server.ip_address.to_s
when 'XXX.XXX.XXX.1'
#oms_server.update_attribute(:status, false)
#next_oms_server = OmsServer.where(ip_address: 'XXX.XXX.XXX.2').first
#next_oms_server.update_attribute(:status, true)
when 'XXX.XXX.XXX.2'
#oms_server.update_attribute(:status, false)
#next_oms_server = OmsServer.where(ip_address: 'XXX.XXX.XXX.3').first
#next_oms_server.update_attribute(:status, true)
when 'XXX.XXX.XXX.3'
#oms_server.update_attribute(:status, false)
#next_oms_server = OmsServer.where(ip_address: 'XXX.XXX.XXX.1').first
#next_oms_server.update_attribute(:status, true)
end
end
Now I modify my Savon Client initialization like so:
#oms_server = OmsServer.where(status: true).first
Savon.client(
endpoint: "http://#{#oms_server.ip_address.to_s}:15043/enmac/SOAP",
namespace: '',
convert_request_keys_to: :camelcase,
env_namespace: 'SOAP-ENV',
namespace_identifier: nil,
log: true,
log_level: :info,
pretty_print_xml: true,
read_timeout: 90,
open_timeout: 90,
headers: {
"Accept-Encoding" => "gzip"
}
)
Finally wherever I make the SOAP Request call I will have something like a begin rescue block like so:
begin
# make soap server request
rescue HTTPServerException => e
shift_oms_server
# make soap server request
ensure
shift_oms_server
# make soap server request
end
Forgive my English. The approach above didn't work for me. I will be happy if someone could enlighten me where I went wrong with the above approach or provide a direction to a more simpler and efficient approach.
Ideally I would like to know if it is possible to have more than one endpoints defined using the Savon gem, if yes how?
Thanks in advance.

Related

How to run cURL commands in Rails

I'm using Ruby on Rails 5 and I need to execute the following command in my application:
curl -F 'client_id=126581840734567' -F 'client_secret=678ebe1b3b8081231aab27dff738313' -F 'grant_type=authorization_code' -F 'redirect_uri=https://uri.com/' -F 'code=AQBi4L2Ohy3Q_N3V48OygFm0zb3gEsL985x5TIyDTNDJaLs93BwXiT1tyGYWoCg1HlBDU7ZRjUfLL5HVlzw4G-7YkVEjp6Id2WuqOz0Ylt-k2ADwDC5upH3CGVtHgf2udQhLlfDnQz5NPsnmxjg4bW3PJpW5FaQs8fn1ztgYp-ssfAf6IRt2-sI45ZC8cqqr5K_12y0Nq_Joh0H-tTfVyNLKatIxHPCqRDb3tfqgmxim1Q' https://api.instagram.com/oauth/access_token
so that it returns something like:
{"access_token": "IGQVJYS0k8V6ZACRC10WjYxQWtyMVRZAN8VXamh0RVBZAYi34RkFlOUxXZnTJsbjlEfnFJNmprQThmQ4hTckpFUmJEaXZAnQlNYa25aWURnX3hpO12NV1VMWDNMWmdIT3FicnJfZAVowM3VldlVWZAEViN1ZAidHlyU2VDMUNuMm2V", "user_id": 17231445640157812}
Is there a way to make Rails execute those types of commands? I was trying the following:
uri = URI.parse('https://api.instagram.com/oauth/access_token')
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request.set_form_data({
"client_id" => "126581840734567",
"client_secret" => "678ebe1b3b8081231aab27dff738313",
"grant_type" => "authorization_code",
"redirect_uri" => "http://nace.network/",
"code" => params[:code]
})
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(request)
end
but I get the following error:
end of file reached
in this line:
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(request)
end
You're using HTTPS, so you need to add this to your code:
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
res = http.request(request)
end
But if you don't need persistent connections, you could also use this:
res = Net::HTTP.post_form(uri,
"client_id" => "126581840734567",
"client_secret" => "678ebe1b3b8081231aab27dff738313",
"grant_type" => "authorization_code",
"redirect_uri" => "http://nace.network/",
"code" => params[:code]
)
Also, you could consider using a library like Faraday, which is a lot easier to deal with.
Edit
This is from TinMan's comment below, sound points.
Using cURL from inside Ruby or Rails is extremely valuable. There is an incredible amount of functionality inside cURL that isn't implemented in Rails or Ruby; Even Ruby's HTTP clients have a hard time replicating it, so cURL is very acceptable depending on the needs of the application. And, depending on the application, because cURL is in compiled C, it could easily outrun pure Ruby clients.
Curl is a means of issuing HTTP (or HTTPs) requests from the command line.
You don't want to use CURL in Rails. You want to issue HTTP requests from within Rails. Using curl is okay, it's one way to issue HTTP requests from with Rails.
We can refine that down further to, you want to issue HTTP requests from Ruby. Narrowing/distilling down to the most basic version of the problem is always good to do.
We knew all this already probably - still worth writing down for us all to benefit from!
Use HTTP in Ruby
We want to use a HTTP Client. There are many but, for this I'm going to use Faraday (a gem) 'cause I like it.
You've made a good start with Ruby's built in NET:HTTP but I prefer Faraday's DSL. It results in more readable and extendable code.
So, here is a class! I barely tested this so, use as a starting point. Make sure you write some unit tests for it.
# This is a Plain Old Ruby Object (PORO)
# It will work in Rails but, isn't Rails specific.
require 'faraday' # This require is needed as it's a PORO.
class InstagramOAuth
attr_reader :code
# The code parameter will likely change frequently, so we provide it
# at run time.
def initialize(code)
#code = code
end
def get_token
connection.get('/oauth/access_token') do |request|
request.params[:code] = code
end
end
private
def connection
#connection ||= Faraday.new(
url: instagram_api_url,
params: params,
ssl: { :ca_path => https_certificate_location }
)
end
def instagram_api_url
#url ||= 'https://api.instagram.com'
end
# You need to find out where these are for your self.
def https_certificate_location
'/usr/lib/ssl/certs'
end
def params
# These params likely won't change to often so we set a write time
# in the class like this.
{
client_id: '126581840734567',
client_secret: '678ebe1b3b8081231aab27dff738313',
grant_type: 'authorization_code',
redirect_uri: 'https://uri.com/'
}
end
end
# How do we use it? Like so
# Your big old authorisation code from your question
code = 'AQBi4L2Ohy3Q_N3V48OygFm0zb3gEsL985x5TIyDTNDJaLs93BwXiT1tyGYWoCg1HlBDU'\
'7ZRjUfLL5HVlzw4G-7YkVEjp6Id2WuqOz0Ylt-k2ADwDC5upH3CGVtHgf2udQhLlfDnQz'\
'5NPsnmxjg4bW3PJpW5FaQs8fn1ztgYp-ssfAf6IRt2-sI45ZC8cqqr5K_12y0Nq_Joh0H'\
'-tTfVyNLKatIxHPCqRDb3tfqgmxim1Q'
# This will return a Faraday::Response object but, what is in it?
response = InstagramOAuth.new(code).get_token
# Now we've got a Hash
response_hash = response.to_hash
puts 'Request made'
puts "Request full URL: #{response_hash[:url]}"
puts "HTTP status code: #{response_hash[:status]}"
puts "HTTP response body: #{response_hash[:body]}"
When I ran the snippet above I got the following. The class works, you just need to tweak the request params until you get what you want. Hopefully the class demonstrates how to send HTTP requests in Ruby/Rails.
Request made
Request full URL: https://api.instagram.com/oauth/access_token?client_id=126581840734567&client_secret=678ebe1b3b8081231aab27dff738313&code=AQBi4L2Ohy3Q_N3V48OygFm0zb3gEsL985x5TIyDTNDJaLs93BwXiT1tyGYWoCg1HlBDU7ZRjUfLL5HVlzw4G-7YkVEjp6Id2WuqOz0Ylt-k2ADwDC5upH3CGVtHgf2udQhLlfDnQz5NPsnmxjg4bW3PJpW5FaQs8fn1ztgYp-ssfAf6IRt2-sI45ZC8cqqr5K_12y0Nq_Joh0H-tTfVyNLKatIxHPCqRDb3tfqgmxim1Q&grant_type=authorization_code&redirect_uri=https%3A%2F%2Furi.com%2F
HTTP status code: 405
HTTP response body:
Additional Reading
. https://lostisland.github.io/faraday/usage/
. https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates

How to solve this error " (pre:svcFault) Service Fault"?

I am trying to call a SOAP api using Savon gem. I am getting the following error: "(pre:svcFault) Service Fault"
I created both the header and message for the request.
Here is the request sent from SoapUI: SoapUI request.
i am getting a true response from SoapUI.
My code is shown below:
class SoapApi
require 'savon'
def self.initialize
header = {
"ebmCID" => "9366498d-bc79-4fad-be2b-fa1a0e84241a",
"ebmMID" => "9366498d-bc79-4fad-be2b-fa1a0e84241a",
"ebmRTID" => "9366498d-bc79-4fad-be2b-fa1a0e84241a",
"ebmSID" => "FMobile-FCUBS",
"ebmTimestamp" => "2019-06-10T12:27:46.1623586Z",
}
message = {
customerId: '00653473'
}
client = Savon.client(
:wsdl => "https://192.168.176.103:8012/tevs/pp.pm.evs.Customer_1.2?wsdl",
:ssl_verify_mode => :none
)
response = client.call(
:get_account_list,
:soap_header => header,
:message => message
)
return response
end
end
And here i am calling the above method:
#index.html.erb
<%=
SoapApi.initialize
puts #response
%>
Where you able to create a valid call using SoapUI (https://www.soapui.org.)? Try this first and make it work.
Next create a call from a plain ruby script - without Rails - which sends the same functional XML as you did in SoapUI before.
Third embed this code into your RoR application.
You can put the following in your client definition for better logging:
client = Savon.client(
:wsdl => "https://192.168.176.103:8012/tevs/pp.pm.evs.Customer_1.2?wsdl",
:ssl_verify_mode => :none,
log: true,
log_level: :debug,
pretty_print_xml: true
)
Compare the output with your working SoapUI example.

ruby Savon use camelcase instead snake

i'm using savon gem for SOAP request building, i need snake_case_elements, but when i inspect it seems converted to CamelCase, am i missing something here? Here is a result
And how i did it:
gls_url = 'https://adi-test.gls-poland.pl/adeplus/pm1/ade_webapi2.php?wsdl'
username = '612305291'
password = 'KGpartt2016'
#client = Savon.client(wsdl: gls_url,
log: true,
logger: Rails.logger,
log_level: :debug,
pretty_print_xml: true)
ats = #client.call(:ade_login, message: {
user_name: username,
user_password: password
})
You can use convert_request_keys_to option to tell Gyoku how to convert the keys (if at all):
#client = Savon.client(wsdl: gls_url, convert_request_keys_to: :none)

How can I have a Rails API set a JWT in a cookie on a client that is running on another domain?

Long time lurker, first time poster here.
There are many good guides and resources about JWTs and how and where to store them. But I'm running into an impasse when it comes to securely storing and sending a JWT between a ReactJS/Flux app running on a Node server and a completely separate Rails API.
It seems most guides tell you to just store the JWT in local storage and pluck it out for every AJAX request you make and pass it along in a header. https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/ warns against this, however, since local storage is not secure and a malicious person could access that token. It recommends storing it in the cookie instead and just letting the web browser pass it along with each request.
That sounds fine to me since from what I understand cookies get conveniently sent along with every request anyway. It means I can just make AJAX requests from my ReactJS app to my Rails API and have the API pluck it out, check it, and do it's thing.*
The problem I'm running into is my Node application doesn't set a cookie from the response it gets back from the Rails API even though the Rails API (running on localhost:3000) returns a Set-Cookie header and sends it back to the ReactJS/Node app (running on localhost:8080).
Here's my login controller action on my Rails API side:
class V1::SessionsController < ApplicationController
def create
user = User.where(email: params[:user][:email]).first!
if user && user.authenticate(params[:user][:password])
token = issue_new_token_for(user)
# I've tried this too.
# cookies[:access_token] = {
# :value => token,
# :expires => 3.days.from_now,
# :domain => 'https://localhost:8080'
# }
response.headers['Set-Cookie'] = "access_token=#{token}"
render json: { user: { id: user.id, email: user.email }, token: token }, status: 200
else
render json: { errors: 'username or password did not match' }, status: 422
end
end
end
The gist of it is it takes an email and password, looks the user up, and generates JWT if the info checks out.
Here's the AJAX request that is calling it from my Node app:
$.ajax({
url: 'http://localhost:3000/v1/login',
method: 'post',
dataType: 'json',
data: {
user: {
email: data.email,
password: data.password
},
callback: '' //required to get around ajax CORS
},
success: function(response){
console.log(response);
},
error: function(response) {
console.log(response);
}
})
Inspecting the response from the Rails API shows it has a Set-Cookie header with a value of access_token=jwt.token.here
Screenshot:
Chrome Dev Tools Inspector Screenshot
However, localhost:8080 does not show any cookies set and subsequent AJAX calls from my Node/React app do not have any cookies being sent along with them.
My question is, what piece(s) am I misunderstanding. What would I have to do to make storing JWTs in cookies work in this scenario?
A follow-up question: assuming storing the JWT in a cookie is not an option, what potential security risks could there be with storing the JWT in local storage (assuming I don't put any sensitive info in the JWT and they all expire in some arbitrary amount of time)?
*this may be a fundamental misunderstanding I have. Please set me straight if I have this wrong.
Side-notes that may be of interest:
My Rails API has CORS setup to only allow traffic from localhost:8080
in development.
In production, the Node/React app will probably be
running on a main domain (example.com) and the Rails API will be
running on a sub domain (api.example.com), but I haven't gotten that
far yet.
There's nothing sensitive in my JWT, so local storage is an
option, but I want to know why my setup doesn't work with cookies.
Update elithrar submitted an answer that worked:
I needed to modify my AJAX request with xhrFields and crossDomain as well as tell jQuery to support cors:
$.support.cors = true;
$.ajax({
url: 'http://localhost:3000/v1/login',
method: 'post',
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true,
data: {
user: {
email: data.email,
password: data.password
}
},
success: function(response){
console.log(response);
},
error: function(response) {
console.log(response);
}
})
And I added credentials: true and expose: true to my Rack Cors configuration on my Rails API (the * is only for my development environment):
config.middleware.insert_before 0, 'Rack::Cors' do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :put, :path, :options], credentials: true, expose: true
end
end

Ruby: Savon AddressFilter mismatch at the EndpointDispatcher SOAP API

I am new to SOAP and I am having lots of trouble with Savon.
The API I am trying to access has really simplistic documentation: http://www.sona-systems.com/support/docs/sona_api_docs.pdf
When making a call, the API expects authentication in the parameters being sent. Here is my code:
client = Savon.client(
wsdl: "https://school.sona-systems.com/services/SonaAPI.svc?singleWsdl",
soap_header: {'To:' => "http://www.sona-systems.com/"},
pretty_print_xml: true,
soap_version: 2
)
response = client.call(:get_study_list) do
message username: "foo", password: "bar"
end
I am getting the following error:
Savon::SOAPFault: (s:Sender) The message with To '' cannot be processed at the receiver, due to an AddressFilter mismatch at the EndpointDispatcher. Check that the sender and receiver's EndpointAddresses agree.
what is the soap_header: suppose to do?
I also cannot get to the url you provided. you should add log: true, log_level: :debug to your client definition to get more information.
try this:
client = Savon.client(
wsdl: "https://kellogg-elab.sona-systems.com/services/SonaAPI.svc?wsdl",
pretty_print_xml: true,
log: true,
log_level: :debug
)
puts client.operations
# then call a method of the service like this
resp = client.call(:get_study_list,
message: {username: "foo", password: "bar"}
)

Resources