UPnP – Ruby Integration

[EDIT:This has been released as gem. check the code page]

At the moment i’m trying to create a ruby module to integrate some upnp functionality in the language.

I would like to do so to enhance the experience with rtorrent, a really nerdish torrent client. Ok, it uses curses, as interface, but it is just great, never crashed in 6 months and had a tiny memory footprint.
The idea is to connect to the local router and gather information about the port mappings and the data currently flowing in the local network and trough estimation change the upload rate.
Useful to use the bandwidth when it is unused, as long as i pay it i want to use it!  Here is how I have built such a module on Mac OS X 10.5.3 with the apple’s version of ruby (1.8). To avoid to write the code i’ll use the miniupnp (1.0) library and the utility swig (1.3.35)
The first is a upnp library which interacts with the IGDs (Internet Gateway Device) and the latter is an wrapper builder that will write all the glue code (C\iff Ruby) in my place.

Get miniupnp to compile

The first thing is to get the library to compile on my mac. The distribution makefile has some minor flaws, I have modified it to build the dynamic library with this patch makefile-osx.patch
--- a/Makefile
+++ b/Makefile
 HEADERS = miniupnpc.h miniwget.h upnpcommands.h igd_desc_parse.h \
 upnpreplyparse.h upnperrors.h
 LIBRARY = libminiupnpc.a
-SHAREDLIBRARY = libminiupnpc.so
+SHAREDLIBRARY = libminiupnpc.dylib
 SONAME = $(SHAREDLIBRARY).$(APIVERSION)
 EXECUTABLES = upnpc-static upnpc-shared \
 testminixml minixmlvalid testupnpreplyparse
@@ -83,7 +83,7 @@ $(LIBRARY):	$(LIBOBJS)
 $(AR) crs $@ $?

 $(SHAREDLIBRARY):	$(LIBOBJS)
-	$(CC) -shared -Wl,-soname,$(SONAME) -o $@ $^
+	$(CC) -dynamiclib -o $@ $^

 upnpc-static:	upnpc.o $(LIBRARY)
 $(CC) -o $@ $^
As the build completes everything is almost done. Another patch has to be applied to avoid a swig warning for a memory leak (miniupnp.g-memleak.patch).
--- a/miniupnpc.h
+++ b/miniupnpc.h
@@ -16,7 +16,7 @@ extern "C" {
 #endif

 /* Structures definitions : */
-struct UPNParg { const char * elt; const char * val; };
+struct UPNParg { char * elt; char * val; };

 int simpleUPnPcommand(int, const char *, const char *,
 const char *, struct UPNParg *,

Get Glue Code

Then we have to write a swig interface file, describing what should be put in the ruby module writing this in the file upnp.i
%module miniupnp
%{
#include "miniupnpc.h"
%}

%include "cpointer.i"
%pointer_functions(unsigned int, uintp);

%import declspec.h
%include miniupnpc.h
%include upnpcommands.h
%include igd_desc_parse.h

Make the Module

This will create a module that will contain all the functions from miniupnpc.h, upnpcommands.h and igd_desc_parse.h and a struct i have added to ease the use of the library. Swig is then called

swig -ruby upnp.i
gcc -fPIC upnp_wrap.c -c -o upnp_wrap.o -I/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0/

Generating the file upnp_wrap.c glue code which must be compiled with the ruby includes. From this point there is a decision to take, if the miniupnp library is to be linked to the module as an external shared library or be included in it as static code.
To link it dynamically

gcc -fPIC -bundle -undefined dynamic_lookup upnp_wrap.o -o miniupnp.bundle -lruby -lminiupnpc -L<miniupnpc install path>

And to link it statically

gcc -fPIC -bundle -undefined dynamic_lookup upnp_wrap.o -o miniupnp.bundle -lruby miniwget.o minixml.o igd_desc_parse.o minisoap.o miniupnpc.o upnpreplyparse.o upnpcommands.o minissdpc.o upnperrors.o

Test it

Now is time to use the newly created module. This can be done in 2 ways too. Installing it in the ruby library path or configuring the library path.


# system install
cp miniupnp.bundle /Library/Ruby/Site/1.8/
# setup path, bundle_path is where the
# bundle is (for instance $PWD)
export RUBYLIB=bundle_path

Here there is a little script to test that everything is working
# The feature name begins with a lowercase letter...
require 'miniupnp'

# max time to wait for the upnp devices
MAX_WAIT_TIME = 1000

# discover upnp
list = Miniupnp.upnpDiscover(MAX_WAIT_TIME,nil,nil)
if list == nil then
    puts "No UPNP device found"
    exit 1
end
puts "URL : #{list.descURL}"

# get valid internet gateway device
urls  = Miniupnp::UPNPUrls.new
datas = Miniupnp::IGDdatas.new
lan = ""*16
r = Miniupnp.UPNP_GetValidIGD(list,urls,datas,lan,16)
case r
when 0
    puts "No IGD found"
    exit 2
when 1
    puts "A valid connected IGD has been found"
when 2
    puts "A valid IGD has been found but it is reported as not connected"
when 3
    puts "An UPnP device has been found but was not recognized as an IGD"
    exit 3
end

puts "Client LAN IP: #{lan}"
ext=""*16
r = Miniupnp.UPNP_GetExternalIPAddress(urls.controlURL,datas.servicetype,ext)
if r == 0 then
    puts "External IP: #{ext}"
else
    puts "Error while retriving the external ip address"
end

# print urls
puts "*** urls"
puts "controlURL: #{urls.controlURL}"
puts "ipcondescURL: #{urls.ipcondescURL}"
puts "controlURL_CIF: #{urls.controlURL_CIF}"

# print datas
puts "*** datas"
puts "cureltname: #{datas.cureltname}"
puts "urlbase: #{datas.urlbase}"
puts "level: #{datas.level}"
puts "state: #{datas.state}"
puts "controlurl_CIF: #{datas.controlurl_CIF}"
puts "eventsuburl_CIF: #{datas.eventsuburl_CIF}"
puts "scpdurl_CIF: #{datas.scpdurl_CIF}"
puts "servicetype_CIF: #{datas.servicetype_CIF}"
puts "devicetype_CIF: #{datas.devicetype_CIF}"
puts "controlurl: #{datas.controlurl}"
puts "eventsuburl: #{datas.eventsuburl}"
puts "scpdurl: #{datas.scpdurl}"
puts "servicetype: #{datas.servicetype}"
puts "devicetype: #{datas.devicetype}"

puts "Get maximum bandwith"
up=Miniupnp.new_uintp()
down=Miniupnp.new_uintp()
Miniupnp.UPNP_GetLinkLayerMaxBitRates(urls.controlURL_CIF,datas.servicetype_CIF,down,up)
puts "up: #{Miniupnp.uintp_value(up)} down: #{Miniupnp.uintp_value(down)}"
Miniupnp.delete_uintp(up)
Miniupnp.delete_uintp(down)

# release memory
Miniupnp.freeUPNPDevlist(list)
Miniupnp.FreeUPNPUrls(urls)
I can’t get wordpress to display correctly the content of lan and ext variables, it should be “” repeated 16 times. The result should be something like this
URL : http://192.168.0.1:49152/gateway.xml
A valid connected IGD has been found
Client LAN IP: 192.168.0.100
External IP: ***.***.***.243
*** urls
controlURL: http://192.168.0.1:49152/upnp/control/WANIPConnection
ipcondescURL: http://192.168.0.1:49152/ipcfg.xml
controlURL_CIF: http://192.168.0.1:49152/upnp/control/
WANCommonInterfaceConfig
*** datas
cureltname: presentationURL
urlbase: http://192.168.0.1:49152
level: 0
state: 3
controlurl_CIF: /upnp/control/WANCommonInterfaceConfig
eventsuburl_CIF: /upnp/event/WANCommonInterfaceConfig
scpdurl_CIF: /cmnicfg.xml
servicetype_CIF: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1
devicetype_CIF: urn:schemas-upnp-org:device:WANDevice:1wayDevice:1
controlurl: /upnp/control/WANIPConnection
eventsuburl: /upnp/event/WANIPConnection
scpdurl: /ipcfg.xml
servicetype: urn:schemas-upnp-org:service:WANIPConnection:1
devicetype: urn:schemas-upnp-org:device:WANConnectionDevice:1

Maximum bandwith
up: 352000
down: 2464000

This is just a start, I’ll make more testing and i’ll make sure it works. There may be some more things to add to upnp.i

Full code can be found in the code page
Edit 15-06-08: Module name changed to miniupnp from upnp

[Continued by: UPnP Wrapper]
[EDIT:This has been released as gem. check the code page]

Advertisements

, , , , ,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: