Skip to content

Commit

Permalink
allow non KingSoa service endpoints, returning plain text. Configure …
Browse files Browse the repository at this point in the history
…rack middleware path for detecting incoming soa calls.

added documentation
  • Loading branch information
schorsch committed Jun 25, 2010
1 parent ebbbbde commit 8c9f1b5
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 47 deletions.
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,4 +1,4 @@
Copyright (c) 2009 Dirk Breuer Copyright (c) 2010 Georg Leciejewski


Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
Expand Down
17 changes: 14 additions & 3 deletions README.rdoc
Expand Up @@ -66,7 +66,7 @@ to provide some minimal access restriction. To make it secure you should either
use https in public or hide the endpoints somwhere on your farm. use https in public or hide the endpoints somwhere on your farm.


Service endpoints receiving such calls need to use the provided rack middleware. Service endpoints receiving such calls need to use the provided rack middleware.
The middleware is doing the authentication, executing the service class and As the middleware is doing the authentication, executing the service class and
returns values or errors. returns values or errors.


=== Local Services === Local Services
Expand All @@ -80,6 +80,12 @@ The service is put onto a resque queue and somewhere in your cloud you should
have a worker looking for it. The service class should also have the resque have a worker looking for it. The service class should also have the resque
@queue attribute set so the job can be resceduled if it fails. @queue attribute set so the job can be resceduled if it fails.


=== Gotchas

* make sure to define the service on the sender side with the appropriate url
* define the local service(called from remote) with the right auth key
* double check your auth keys(a string is not an int), to be save use "strings" esp. when loading from yml

== Integration == Integration


Just think of a buch of small sinatra or specialized rails apps, each having Just think of a buch of small sinatra or specialized rails apps, each having
Expand Down Expand Up @@ -119,13 +125,18 @@ The base is just:
use KingSoa::Rack::Middleware use KingSoa::Rack::Middleware


The service definition should of course also be done, see rails example. The service definition should of course also be done, see rails example.


== ToDo

* better error logging
* a central server & clients getting definitions from it

== Note on Patches/Pull Requests == Note on Patches/Pull Requests


* Fork the project. * Fork the project.
* Make your feature addition or bug fix. * Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a future version unintentionally. * Add tests for it. This is important so I don't break it in a future version unintentionally.
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a branch or commit by itself so I can ignore when I pull)
* Send me a pull request. Bonus points for topic branches. * Send me a pull request. Bonus points for topic branches.


== Copyright == Copyright
Expand Down
5 changes: 4 additions & 1 deletion lib/king_soa.rb
Expand Up @@ -3,6 +3,9 @@
require 'json' require 'json'
require 'typhoeus' require 'typhoeus'
require 'active_support/inflector' require 'active_support/inflector'
# Rails 3.0
#require 'active_support'
#require 'active_support/core_ext/string'


require 'king_soa/registry' require 'king_soa/registry'
require 'king_soa/service' require 'king_soa/service'
Expand All @@ -27,7 +30,7 @@ def method_missing(meth, *args, &blk) # :nodoc:
if service = Registry[meth] if service = Registry[meth]
service.perform(*args) service.perform(*args)
else else
super super(meth, *args, &blk)
end end
end end
end end
Expand Down
15 changes: 11 additions & 4 deletions lib/king_soa/rack/middleware.rb
@@ -1,14 +1,22 @@
module KingSoa::Rack module KingSoa::Rack
class Middleware class Middleware


def initialize(app) # === Params
# app:: Application to call next
# config<Hash{Symbol=>String}>::
# === config hash
# :endpoint_path<RegEx>:: Path which is getting all incoming soa requests.
# Defaults to /^\/soa/ => /soa
# Make sure your service url's have it set too.
def initialize(app, config={})
@app = app @app = app
@config = config
@config[:endpoint_path] ||= /^\/soa/
end end


# Takes incoming soa requests and calls the passed in method with given params # Takes incoming soa requests and calls the passed in method with given params
def call(env) def call(env)
# Hoth::Logger.debug "env: #{env.inspect}" if env["PATH_INFO"] =~ @config[:endpoint_path]
if env["PATH_INFO"] =~ /^\/soa/
begin begin
req = Rack::Request.new(env) req = Rack::Request.new(env)
# find service # find service
Expand All @@ -29,7 +37,6 @@ def call(env)
] ]


rescue Exception => e rescue Exception => e
#Hoth::Logger.debug "e: #{e.message}"
if service if service
encoded_error = service.encode({"error" => e}) encoded_error = service.encode({"error" => e})
[500, {'Content-Type' => 'application/json', 'Content-Length' => "#{encoded_error.length}"}, [encoded_error]] [500, {'Content-Type' => 'application/json', 'Content-Length' => "#{encoded_error.length}"}, [encoded_error]]
Expand Down
67 changes: 42 additions & 25 deletions lib/king_soa/service.rb
@@ -1,11 +1,21 @@
module KingSoa module KingSoa
class Service class Service
# endpoint url
attr_accessor :debug, :name, :auth, :queue # name<String/Symbol>:: name of the service class to call
# auth<String/Int>:: password for the remote service. Used by rack middleware
# to authentify the callee
# url<String>:: Url where the service is located. If the rack middleware is
# used on the receiving side, make sure to append /soa to the url so the
# middleware can grab the incoming call. A custom endpoint path can be set in
# the middleware.
# queue<Boolean>:: turn on queueing for this service call. The incoming
# request(className+parameter) will be put onto a resque queue
# debug<Boolean>:: turn on verbose outpur for typhoeus request
attr_accessor :debug, :name, :auth, :queue, :url


def initialize(opts) def initialize(opts)
self.name = opts[:name].to_sym self.name = opts[:name].to_sym
[:url, :queue,:auth, :debug ].each do |opt| [:url, :queue, :auth, :debug ].each do |opt|
self.send("#{opt}=", opts[opt]) if opts[opt] self.send("#{opt}=", opts[opt]) if opts[opt]
end end
end end
Expand All @@ -18,9 +28,19 @@ def call_remote(*args)
resp_code = request.perform resp_code = request.perform
case resp_code case resp_code
when 200 when 200
return self.decode(request.response_body)["result"] if request.response_header.include?('Content-Type: application/json')
#decode incoming json .. most likely from KingSoa's rack middleware
return self.decode(request.response_body)["result"]
else # return plain body
return request.response_body
end
else else
return self.decode(request.response_body)["error"] if request.response_header.include?('Content-Type: application/json')
#decode incoming json .. most likely from KingSoa's rack middleware
return self.decode(request.response_body)["error"]
else # return plain body
return request.response_body
end
end end
end end


Expand All @@ -36,13 +56,17 @@ def add_to_queue(*args)
# * local by calling perform method on a class # * local by calling perform method on a class
# * put a job onto a queue # * put a job onto a queue
# === Parameter # === Parameter
# args:: whatever arguments the service methods recieves. Those are later json # args:: whatever arguments the service methods receives. A local service/method
# encoded for remote or queued methods # gets thems as splatted params. For a remote service they are converted to
# json
# === Returns
# <nil> for queued services dont answer
# <mixed> Whatever the method/service
def perform(*args) def perform(*args)
if queue if queue
add_to_queue(*args) add_to_queue(*args)
return nil return nil
else else # call the local class if present, else got remote
result = local_class ? local_class.send(:perform, *args) : call_remote(*args) result = local_class ? local_class.send(:perform, *args) : call_remote(*args)
return result return result
end end
Expand All @@ -57,16 +81,17 @@ def local_class
end end
end end


# Return the classname infered from the camelized service name. # Return the class name infered from the camelized service name.
# A service named: save_attachment => class SaveAttachment # === Example
# save_attachment => class SaveAttachment
def local_class_name def local_class_name
self.name.to_s.camelize self.name.to_s.camelize
end end


# Set options for the typhoeus curl request # Set options for the typhoeus curl request
# === Parameter # === Parameter
# req<Typhoeus::Easy>:: request object # req<Typhoeus::Easy>:: request object
# args<Array[]>:: the arguments for the soa method, will be json encoded and added to post body # args<Array[]>:: arguments for the soa method, added to post body json encoded
def set_request_opts(req, args) def set_request_opts(req, args)
req.url = url req.url = url
req.method = :post req.method = :post
Expand All @@ -77,20 +102,12 @@ def set_request_opts(req, args)
req.verbose = 1 if debug req.verbose = 1 if debug
end end


# Url receiving the request # Params for a soa request consist of following values:
# TODO. if not present try to grab from endpoint # name => name of the soa class to call
def url # args => arguments for the soa class method -> Class.perform(args)
@url # auth => an authentication key. something like a api key or pass. To make
end # it really secure you MUST use https or hide your soa endpoints from public
def url=(url) # web acces
@url = "#{url}/soa"
end

# The params for each soa request consist of following values:
# name => the name of the method to call
# args => the arguments for the soa class method
# auth => an authentication key. something like a api key or pass. To make
# it really secure you MUST use https or do not expose your soa endpoints
# #
# ==== Parameter # ==== Parameter
# payload<Hash|Array|String>:: will be json encoded # payload<Hash|Array|String>:: will be json encoded
Expand Down
10 changes: 9 additions & 1 deletion spec/king_soa/rack/middleware_spec.rb
Expand Up @@ -25,4 +25,12 @@
env = {"PATH_INFO" => "/soa"} env = {"PATH_INFO" => "/soa"}
# KingSoa.should_receive(:find).and_return(@service) # KingSoa.should_receive(:find).and_return(@service)
end end
end end

#
# von bumi
#referer = "http://google.com"
# env = Rack::MockRequest.env_for "http://payango.com", :method => "GET", "HTTP_REFERER" => referer
# app = mock('app')
# app.expects(:call).with(has_entry("X_EXTERNAL_REFERER", referer))
# response = Middleware::Referer.new(app).call(env)
26 changes: 17 additions & 9 deletions spec/king_soa/service_spec.rb
@@ -1,11 +1,14 @@
require File.dirname(__FILE__) + '/../spec_helper.rb' require File.dirname(__FILE__) + '/../spec_helper.rb'


describe KingSoa::Service do describe KingSoa::Service, 'in general' do

before(:each) do before(:each) do
end end


it "should init" do it "should not init without name" do

lambda {
s = KingSoa::Service.new()
}.should raise_error
end end
end end


Expand Down Expand Up @@ -33,18 +36,23 @@
stop_test_server stop_test_server
end end


it "should call a service remote" do it "should call a remote service" do
s = KingSoa::Service.new(:name=>:soa_test_service, :url=>test_url, :auth=>'12345') s = KingSoa::Service.new(:name=>:soa_test_service, :url=>test_soa_url, :auth=>'12345')
s.perform(1,2,3).should == [1,2,3] s.perform(1,2,3).should == [1,2,3]
end end


it "should call a service remote and return auth error" do it "should call a remote service and return auth error" do
s = KingSoa::Service.new(:name=>:soa_test_service, :url=>test_url, :auth=>'wrong') s = KingSoa::Service.new(:name=>:soa_test_service, :url=>test_soa_url, :auth=>'wrong')
s.perform(1,2,3).should == "Please provide a valid authentication key" s.perform(1,2,3).should == "Please provide a valid authentication key"
end end


it "should call a service remote and return auth error" do it "should call a service remote and return not found error" do
s = KingSoa::Service.new(:name=>:wrong_service, :url=>test_url, :auth=>'12345') s = KingSoa::Service.new(:name=>:wrong_service, :url=>test_soa_url, :auth=>'12345')
s.perform(1,2,3).should include("The service: wrong_service could not be found") s.perform(1,2,3).should include("The service: wrong_service could not be found")
end end

it "should call a remote service without using middleware and returning plain text" do
s = KingSoa::Service.new(:name=>:non_soa_test_service, :url=> "#{test_url}/non_json_response")
s.perform().should == "<h1>hello World</h1>"
end
end end
11 changes: 9 additions & 2 deletions spec/server/app.rb
Expand Up @@ -14,15 +14,22 @@
exit! exit!
end end


####################################### # A non king_soa method => rack middleware is not used
# Somewhere in you app define services # Still such methods can be called and their result is returned as plain text
post '/non_json_response' do
"<h1>hello World</h1>"
end

################################################################################
# Somewhere in you app you define a local service, receiving the incoming call
# #
# setup test registry # setup test registry
service = KingSoa::Service.new(:name=>'soa_test_service', :auth => '12345') service = KingSoa::Service.new(:name=>'soa_test_service', :auth => '12345')
KingSoa::Registry << service KingSoa::Registry << service


# the local soa class beeing called # the local soa class beeing called
class SoaTestService class SoaTestService
#simply return all given parameters
def self.perform(param1, param2, param3) def self.perform(param1, param2, param3)
return [param1, param2, param3] return [param1, param2, param3]
end end
Expand Down
6 changes: 5 additions & 1 deletion spec/spec_helper.rb
Expand Up @@ -9,14 +9,18 @@
# for starting the test sinatra server # for starting the test sinatra server
require 'open3' require 'open3'


def test_soa_url
"#{test_url}/soa"
end

def test_url def test_url
'http://localhost:4567' 'http://localhost:4567'
end end


# Start a local test sinatra app receiving the real requests .. no mocking here # Start a local test sinatra app receiving the real requests .. no mocking here
# the server could also be started manually with: # the server could also be started manually with:
# ruby spec/server/app # ruby spec/server/app
def start_test_server(wait_time=1) def start_test_server(wait_time=3)
# start sinatra test server # start sinatra test server
Dir.chdir(File.dirname(__FILE__) + '/server/') do Dir.chdir(File.dirname(__FILE__) + '/server/') do
@in, @rackup, @err = Open3.popen3("ruby app.rb") @in, @rackup, @err = Open3.popen3("ruby app.rb")
Expand Down

0 comments on commit 8c9f1b5

Please sign in to comment.