SOAP童貞を卒業する

今回は、初めてのSOAPプログラミングを何も知識が無いままやってみると成功したので、それについて書こうと思います。


そもそも、SOAPとは何か。
SOAP (プロトコル) - Wikipedia
簡単に言うと、XML-RPCの凄い版という事になるのでしょうか?
少なくともXMLスキーマで、何かしらのプロトコルの上に乗せて飛ばします。XML-RPCと同じくHTTPが多いようですが、その限りでは無いようです。


突然ですが、今日の夕方頃から私が始めた事は一見SOAPと全く関係が無い様に見えた事でした。
何をやり始めたのか・・・ですが、前日のエントリにちらっと出したUPnPです。


話をいきなり脱線させて、UPnPについて述べる事にします。

UPnPとは??

http://ja.wikipedia.org/wiki/UPnP
http://bb.watch.impress.co.jp/cda/bbword/10002.html


UPnP」という単語は少なくとも聞いた事があると言う方は多いと思います。
UPnPのコンセプトは、ユビキタスには重要です。


特に今回は、その中のうちNAPT Traversalの手法としてUPnPを捉えようと思います。
ただ、それは何も難しい事では無くて、従来逐一手動で行っていたNAPTにおける静的ポート変換ルール設定、mappingを自動で行える様にする・・・という事にあります。


そこに何故SOAPが絡むのか疑問に思われるかもしれません。
先のwikipediaの記事内に

サービスの持つ機能を呼び出すアクションと、デバイスの状態変数を問合せるクエリーがある。
これらのメッセージは、XMLによって記述されたSOAPが使われる。

という一文があります。
お分かりだと思いますが、UPnPを使ってルータに対してクエリを投げる時、それを行う為にSOAPを使う事になるのです。


UPnP対応のルータは、意外にもspecificなSOAPサーバに成っているのです。
最も身近なSOAPサーバと言えるかも(?)しれません。

実験を行います

今回特に重要な事は、
1.UPnP対応ルータを用いる
2.尚且つUPnPをONにする
3.ご自分の責任でどうぞ
の3つです。


以下にrubyのコードを張っていきますが、一切汎用的な物にしていません。(そうでないと無駄に長くなるし、今回の場合はべたな方が良いと思います)

ルータのControl URLを教えてもらう

SOAPを介してコントロールする為にも、ルータから適切なアドレスを教えてもらわなければ成りません。それを行うのが次のコードです。

//File name getSOAPxml.java
import java.net.*;

public class getSOAPxml
{
    public static void main(String[] args)
    {
	try{
	    String mes1 = new String();
	    String mes2 = new String();
	    mes1 = "";
	    mes1 += "M-SEARCH * HTTP/1.1\r\n";
	    mes1 += "MX: 3\r\n";
	    mes1 += "HOST: 239.255.255.250:1900\r\n";
	    mes1 += "MAN: \"ssdp:discover\"\r\n";
	    mes1 += "ST: urn:schemas-upnp-org:service:WANPPPConnection:1\r\n";
	    mes1 += "\r\n";

	    mes2 = "";
	    mes2 += "M-SEARCH * HTTP/1.1\r\n";
	    mes2 += "MX: 3\r\n";
	    mes2 += "HOST: 239.255.255.250:1900\r\n";
	    mes2 += "MAN: \"ssdp:discover\"\r\n";
	    mes2 += "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n";
	    mes2 += "\r\n";

	    InetAddress group = InetAddress.getByName("239.255.255.250");

	    //MulticastSocket s = new MulticastSocket(1900);
	    MulticastSocket s = new MulticastSocket();
	    s.joinGroup(group);
	    
	    DatagramPacket pack1 = new DatagramPacket(mes1.getBytes("US-ASCII"),
						      mes1.length(),
						      group,1900);
	    DatagramPacket pack2 = new DatagramPacket(mes2.getBytes("US-ASCII"),
						      mes2.length(),
						      group,1900);
	    s.send(pack1);
	    s.send(pack2);
	    
	    //System.out.println("send!!");

	    byte[] buf = new byte[10000];
	    DatagramPacket recv = new DatagramPacket(buf,buf.length);
	    s.receive(recv);
	    System.out.println(new String(buf));
	    s.leaveGroup(group);
	}
	catch(Exception e){
	    System.out.println(e);
	}
    }
}
require "socket"
require "ipaddr"

udp = UDPSocket.open()
saddr = Socket.pack_sockaddr_in(1900,"239.255.255.250")

mif = IPAddr.new("0.0.0.0").hton
udp.setsockopt(Socket::IPPROTO_IP,
               Socket::IP_MULTICAST_IF,
               mif)

mes1 = ""
mes1 += "M-SEARCH * HTTP/1.1\r\n"
mes1 += "MX: 3\r\n"
mes1 += "HOST: 239.255.255.250:1900\r\n"
mes1 += "MAN: \"ssdp:discover\"\r\n"
mes1 += "ST: urn:schemas-upnp-org:service:WANPPPConnection:1\r\n"
mes1 += "\r\n"

mes2 = ""
mes2 += "M-SEARCH * HTTP/1.1\r\n"
mes2 += "MX: 3\r\n"
mes2 += "HOST: 239.255.255.250:1900\r\n"
mes2 += "MAN: \"ssdp:discover\"\r\n"
mes2 += "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n"
mes2 += "\r\n"

udp.send(mes1,0,saddr)
udp.send(mes2,0,saddr)

mes,from = udp.recvfrom(1000)
p mes
p from
udp.close

239.255.255.250というマルチキャストアドレスに対して、UDPマルチキャストを行います。
WANPPPConnectionとWANIPConnection両方投げているのは、どちらか一方しか使えない状況もあるらしいからです。


これを実行すると、数秒(うちのルータではほぼ5秒で)メッセージが返ってきます。
その内容は次の様になっています。

HTTP/1.1 200 OK
ST:urn:schemas-upnp-org:service:WANPPPConnection:1
USN:uuid:xxxx::urn:schemas-upnp-org:service:WANPPPConnection:1
Location:http://192.168.11.1:80/igd.xml <- ここにルータの情報が載っている。
Server:BBR-4HG/1.33 Release 0003 UPnP/1.0 UPnP-Device-Host/1.0
EXT:
Cache-Control:max-age=60

うちのルータ名が丸出しですが、バッファローのBBR-4HGを用いています。こいつはUPnP対応ルータです。


さて、返ってきたメッセージのうち、Location部分が重要です。
適当にブラウザやらで自身のルータのLocationにアクセスしてみてください。

WANPPPConnectionサービスURL/WANIPConnectionサービスURLを入手しよう

上記XMLにアクセスすると何やら情報が出て来ます。
今回利用するのは
urn:schemas-upnp-org:service:WANPPPConnection:1
もしくは
urn:schemas-upnp-org:service:WANIPConnection:1
のControl URLです。


私が上記XML(igd.xml)にアクセスした時に得られたうち、必要な部分が以下です。

<service>
  <serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
  <serviceId>urn:upnp-org:serviceId:WANPPPConn1</serviceId>
  <SCPDURL>/igd_wpc.xml</SCPDURL>
  <controlURL>
    http://192.168.11.1:5440/upnp/control?WANPPPConnection <- これ!!
  </controlURL>
  <eventSubURL>
    http://192.168.11.1:5440/upnp/event?WANPPPConnection
  </eventSubURL>
</service>

<service>
  <serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
  <serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
  <SCPDURL>/igd_wic.xml</SCPDURL>
  <controlURL>
    http://192.168.11.1:5440/upnp/control?WANIPConnection <- これ!!
  </controlURL>
  <eventSubURL>
    http://192.168.11.1:5440/upnp/control?WANIPConnection
  </eventSubURL>
</service>

今回は、WANPPPConnectionの方を用いる事にしましょう。

SOAPの練習として、ルータに割り振られたWANアドレスを取得する

さて、今回私はRubyを使っているのですが、Ruby SOAPとかでググると意味不明な記事が大量に出てきて大変イライラしてお腹が空いてきたりと大変な目にあいました。


面倒くさい事この上無かったので、XML on HTTP over TCPを直にやってやろうかと思ったのですが、幸いRubyにはHTTPでPOSTを行えるモジュールがあったので、それを使う事にしました。


ちょっと手間が減ったので良しとしましょう。以下がRubyのコードです。

require "net/http"

http = Net::HTTP.new("192.168.11.1",5440)
targ_url = "/upnp/control?WANPPPConnection"

#WANIPConnectionを行う場合は書き換えてください。
#このヘッダが無いと失敗します。
soap_header = {"SOAPAction" => "urn:schemas-upnp-org:service:WANPPPConnection:1#GetExternalIPAddress"}

xml_body = ""
xml_body += '<?xml version="1.0"?>'
xml_body += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
xml_body += '<s:Body>'
xml_body += '<m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1"></m:GetExternalIPAddress>'
xml_body += '</s:Body>'
xml_body += '</s:Envelope>'

response = http.post(targ_url,xml_body,soap_header)
puts response.body

今回はSOAPでGetExternalIPAddressを呼び出しました。名前のまんまです。これでWANアドレスが得られます。


実際にレスポンスを見てみましょう。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
<u:GetExternalIPAddressResponse  xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewExternalIPAddress>x.x.x.x</NewExternalIPAddress>
</u:GetExternalIPAddressResponse>
</s:Body> </s:Envelope>

NewExternalIPAddress要素が、WANアドレスに成ります。
取得出来たものと、WANアドレス確認ページ(ex. http://www.axisnetworks.biz/tools/gip/)にアクセスして得られた物とを比べて見てください。


どうですか??

肝心のMappingを行ってみる

さて、うちのルータはWAN側80番ポートとLAN側80番ポートでmappingを行っていません。
今回は、そのmappingを行う事にします。これで、先ほど得たグローバルIPアドレスを用いてWANからうちのWebサーバにアクセス出来る様に成ります。


以下のrubyコードがそれを行います

require "net/http"

http = Net::HTTP.new("192.168.11.1",5440)
targ_url = "/upnp/control?WANPPPConnection"
soap_header = {"SOAPAction" => "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"}

xml_body = ""
xml_body+= '<?xml version="1.0"?>'
xml_body+= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
xml_body+= '<s:Body>'
xml_body+= '<m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">'
xml_body+= '<NewRemoteHost></NewRemoteHost>'
xml_body+= '<NewExternalPort>80</NewExternalPort>'
xml_body+= '<NewProtocol>TCP</NewProtocol>'
xml_body+= '<NewInternalPort>80</NewInternalPort>'
xml_body+= '<NewInternalClient>192.168.11.3</NewInternalClient>'
xml_body+= '<NewEnabled>1</NewEnabled>'
xml_body+= '<NewPortMappingDescription>UPnP Mapping Test!!</NewPortMappingDescription>'
xml_body+= '<NewLeaseDuration>0</NewLeaseDuration>'
xml_body+= '</m:AddPortMapping>'
xml_body+= '</s:Body>'
xml_body+= '</s:Envelope>'

response = http.post(targ_url,xml_body,soap_header)
puts response.body

SOAPAddPortMappingを行います。
NewExternalPortは、WAN側ポート
NewProtocolは、TCPUDPかでプロトコル選択
NewInternalPortは、LAN側マシンのポート
NewInternalClientは、LAN側マシンのIPアドレス
NewPortMappingDescriptionは、ルータの設定画面に表示する文字列のようですが・・・詳しくは分かりません。
となっています。


ですから今回は、TCPのWAN側ポート80と、LAN-IPアドレス192.168.11.3のポート80でmappingを行う・・・という事に成ります。


これを実行してみます。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
<u:AddPortMappingResponse  xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1"/>
</s:Body> </s:Envelope>

Errorが発生したようではないです。実際、これで外部からWebサーバ(80番ポート)にアクセス出来る様に成りました!!

un-mappingする

ところがまぁ私はWebサーバをあんまり外部に晒したくもありません。


ですから、ルータ(うちのルータはBBR-4HGです)の設定ページからmappingを消そうと思ったのですが、どうやらUPnPを使って行った場合は表示されないようです。


・・・まぁ確かにごちゃごちゃと出てくるのは嫌ですが、UPnPで行ったものも出すオプションが在っても良いでしょう。他のルータではどうなっているかは分かりませんが。


ということで、今度もSOAPを用いて、先ほどのmappingを、今度はun-mappingしてみようと思います。
以下がそれを行うRubyコードです。

require "net/http"

http = Net::HTTP.new("192.168.11.1",5440)
targ_url = "/upnp/control?WANPPPConnection"
soap_header = {"SOAPAction" => "urn:schemas-upnp-org:service:WANPPPConnection:1#DeletePortMapping"}

xml_body = ""
xml_body+= '<?xml version="1.0"?>'
xml_body+= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
xml_body+= '<s:Body>'
xml_body+= '<m:DeletePortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">'
xml_body+= '<NewRemoteHost></NewRemoteHost>'
xml_body+= '<NewExternalPort>80</NewExternalPort>'
xml_body+= '<NewProtocol>TCP</NewProtocol>'
xml_body+= '</m:DeletePortMapping>'
xml_body+= '</s:Body>'
xml_body+= '</s:Envelope>'

response = http.post(targ_url,xml_body,soap_header)
puts response.body

今度はDeletePortMappingSOAPで行います。
NewExternalPortが、unmappingしたいWAN側ポートで、
NewProtocolが、unmappingしたプロトコル(TCP or UDP)です。


実際に実行してみましょう。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
<u:DeletePortMappingResponse  xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1"/>
</s:Body> </s:Envelope>

どうやら成功したようです。
実際に外部からWebサーバ(ポート80)にアクセス出来なくなっています。やりました!

まとめ

今回の話は、SOAPとかなんでこんな所で出てくるねん!!という状況にぶち当たりながらもそれを解決したので、折角だから書いてみたという単純なものです。
UPnPについての詳しい話・・・では無いので、完全にこれを鵜呑みにはしないでください。
興味がある方は、UPnPのページを訪れて英語を読んでもらうしか無い様な気がします。


さて、SOAPと言っても、単純にXML on HTTPで、HTTPはまぁTCPでやってれば大概おkなテキストベースアプリケーションレベルプロトコルなわけで、いちいち(それこそrubyの場合だとsoap4r)大げさなライブラリとかに頼らなくても大丈夫ですよ・・・という話です。


や、それは些細な話なんですがw


ともかく、SOAPUPnP(Mapping/UN-Mapping)が同時に出来て良かったなーとかですね。


あと、こういう記事を書いておく事でもtoremoroさん(http://toremoro.tea-nifty.com/の人)がP2P勉強会を催す時にでも日程ぐらいは教えてもらえる様に成るんじゃないかとかそういう目論見が在ったりもします。


UPnPがどれぐらい普及していて、どれぐらいちゃんと使えるか・・・は分かりませんが、激しい手段を用いるよりはまず先に見当する手段かもしれませんね。
#適当に書き上げたので、記事全体は少しづつ形を変えるかもしれません。