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
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.

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.

=== 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
@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

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

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

* Fork the project.
* 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.
* 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.

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

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

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

rescue Exception => e
#Hoth::Logger.debug "e: #{e.message}"
if service
encoded_error = service.encode({"error" => e})
[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
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)
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]
end
end
Expand All @@ -18,9 +28,19 @@ def call_remote(*args)
resp_code = request.perform
case resp_code
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
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

Expand All @@ -36,13 +56,17 @@ def add_to_queue(*args)
# * local by calling perform method on a class
# * put a job onto a queue
# === Parameter
# args:: whatever arguments the service methods recieves. Those are later json
# encoded for remote or queued methods
# args:: whatever arguments the service methods receives. A local service/method
# 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)
if queue
add_to_queue(*args)
return nil
else
else # call the local class if present, else got remote
result = local_class ? local_class.send(:perform, *args) : call_remote(*args)
return result
end
Expand All @@ -57,16 +81,17 @@ def local_class
end
end

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

# Set options for the typhoeus curl request
# === Parameter
# 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)
req.url = url
req.method = :post
Expand All @@ -77,20 +102,12 @@ def set_request_opts(req, args)
req.verbose = 1 if debug
end

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

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

before(:each) do
end

it "should init" do

it "should not init without name" do
lambda {
s = KingSoa::Service.new()
}.should raise_error
end
end

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

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

it "should call a service remote and return auth error" do
s = KingSoa::Service.new(:name=>:soa_test_service, :url=>test_url, :auth=>'wrong')
it "should call a remote service and return auth error" do
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"
end

it "should call a service remote and return auth error" do
s = KingSoa::Service.new(:name=>:wrong_service, :url=>test_url, :auth=>'12345')
it "should call a service remote and return not found error" do
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")
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
11 changes: 9 additions & 2 deletions spec/server/app.rb
Expand Up @@ -14,15 +14,22 @@
exit!
end

#######################################
# Somewhere in you app define services
# A non king_soa method => rack middleware is not used
# 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
service = KingSoa::Service.new(:name=>'soa_test_service', :auth => '12345')
KingSoa::Registry << service

# the local soa class beeing called
class SoaTestService
#simply return all given parameters
def self.perform(param1, param2, param3)
return [param1, param2, param3]
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
require 'open3'

def test_soa_url
"#{test_url}/soa"
end

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

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

0 comments on commit 8c9f1b5

Please sign in to comment.