Monday, November 08, 2010 at 12:05 AM.
system.verbs.builtins.soap.rpc.server
on server (actionURI, adrRequestText, adrResponseText) {
<<Changes:
<<01/31/02; 3:25:22 PM by JES
<<Added a security check by IP address, using betty.rpc.checkClient. Return a client-fault if the IP address check fails.
<<09/03/01; 1:58:16 PM by JES
<<Return a more descriptive error if the parameter names do not match the expected parameter names.
<<05/29/01; 7:35:14 PM by JES
<<Fixed a problem decoding entity-encoded markup.
<<05/04/01; 12:30:14 AM by JES
<<Bug fix: Fully qualified URLs in the value of the SOAPAction header weren't being processed with enough specificity. We now include the server as part of the ODB path to the RPC handler, in order to properly differentiate services from each other.
<<04/11/01; 12:23:32 AM by JES
<<Allow for processing of requests encoded as ISO-8859-1 (Latin1).
<<04/07/01; 7:26:24 PM by JES
<<Fault if a header element has an actor attribute whose value isn't the "special URI" for actor-next. (See section 4.2.2.)
<<04/04/01; 5:45:11 PM by JES
<<Convert to ascii from utf-8 or utf-16 if charset is specified in the Content-Type request header. When generating a response, convert to the same character set as the request.
<<04/03/01; 5:04:25 PM by JES
<<Reject requests which contain more than one Body element. Reject requests with Header after Body. Fixed a bug where the incorrect fault code would be generated when a request included a header with a mustUnderstand attribute.
<<04/01/01; 3:10:28 AM by JES
<<Properly encode void responses with an empty response element, instead of a response element containing an empty param element.
<<A void response element is returned as a zero-length binary whose binary type is unknownType, since returning nil doesn't work.
<<03/29/01; 7:17:58 PM by JES
<<Call the RPC handler using callScript with a record, instead of a list. Adds support for named parameters.
<<03/28/01; 7:13:21 PM by JES
<<Allow for fully qualified URLs in the value of the SOAPAction header. We strip off the http:// and the server, and use just the URI part to find the RPC handler.
<<03/17/01; 8:50:37 PM by JES
<<Add adrRequest to the context the RPC handler is called in, so that the handler can define customNamespace, customNamespaceURI, and customArrayType. Needed for WSDL support. customNamespace, customNamespaceURI, and customArrayType are passed into soap.encode.main so that arrays of structs can be serialized.
<<02/04/01; 5:43:42 PM by JES
<<Verify that the request's methodName is valid, and return a fault if not.
<<11/21/00; 4:02:59 PM by JES
<<Add client and responder to the request table before calling the RPC handler, so the RPC handler can discover the responder name and client's IP address.
local (flfault = false);
local (methodname, responsename, paramrecord = {}, returnvalue);
local (request, adrrequest = @request);
local (response, adrresponse = @response);
local (charset = soap.constants.charsetUsAscii);
bundle { //get character encoding from the Content-type header
if defined (requestHeaders.["Content-type"]) {
local (contentType = soap.stringutils.parseHeader (requestHeaders.["Content-type"]));
if defined (contentType.charset) {
charset = string.lower (contentType.charset)}}};
on soapfault (faultcode, faultstring) {
flfault = true;
local (faultresponse, adrfaultresponse = @faultresponse);
local (adrdocument, adrenvelope, adrbody, adrfaultstruct, adrfaultcode, adrfaultstring);
adrdocument = soap.xmlutils.createResponse (adrfaultresponse);
adrenvelope = soap.xmlutils.addElement (adrdocument, "Envelope");
soap.xmlutils.setNamespacePrefixOfElement (adrenvelope, soap.constants.nsEnvelopePrefix);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsEnvelopePrefix, soap.constants.nsEnvelopeURI);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsSchemaPrefix, soap.constants.nsSchemaURI);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsSchemaDataPrefix, soap.constants.nsSchemaDataURI);
soap.xmlutils.addAttributeValue (adrenvelope, soap.constants.nsEnvelopePrefix +":" + "encodingStyle", soap.constants.nsEncodingURI);
adrbody = soap.xmlutils.addElement (adrenvelope, "Body");
soap.xmlutils.setNamespacePrefixOfElement (adrbody, soap.constants.nsEnvelopePrefix);
adrfaultstruct = soap.xmlutils.addElement (adrbody, "Fault");
soap.xmlutils.setNamespacePrefixOfElement (adrfaultstruct, soap.constants.nsEnvelopePrefix);
adrfaultcode = soap.xmlutils.addElement (adrfaultstruct, "faultcode");
soap.xmlutils.setCharacterData (adrfaultcode, faultcode);
adrfaultstring = soap.xmlutils.addElement (adrfaultstruct, "faultstring");
soap.xmlutils.setCharacterData (adrfaultstring, soap.xmlutils.encodeWithAmpersands (faultstring));
<<bundle //add faultfactor (commented out because it's not required since we're the final destination of the request)
<<local (faultfactor)
<<if actionURI beginsWith "http://"
<<faultfactor = actionURI
<<else //construct the faultfactor from ip, port and actionURI
<<local (ipandport = tcp.myDottedId ())
<<if user.inetd.config.http.port != 80
<<ipandport = ipandport + ":" + user.inetd.config.http.port
<<faultfactor = "http://" + ipandport + actionURI
<<adrfaultfactor = soap.xmlutils.addElement (adrfaultstruct, "faultfactor")
<<soap.xmlutils.setCharacterData (adrfaultfactor, soap.xmlutils.encodeWithAmpersands (faultfactor))
adrResponseText^ = xml.decompile (adrdocument);
return (true)};
bundle { //do security check by IP address
local (pta = parentOf (client));
if not betty.rpc.checkClient (pta) {
soapfault (soap.constants.faultClient, "Access not allowed from " + client + ".")}};
bundle { //decode request
local (adrdocument, adrenvelope, adrheader, adrbody, adrothers, adrmethodcall);
if not flfault { //build document tree from XML text
try {
on fixEntities (s) {
<<Works around a bug in xml.compile, where both " and " are decoded as ". Clearly this shouldn't be the case, but to fix xml-compile at this point is asking for trouble.
s = string.replaceAll (s, "&", "&amp;");
s = string.replaceAll (s, """, "&quot;");
s = string.replaceAll (s, "'", "&apos;");
s = string.replaceAll (s, "<", "&lt;");
s = string.replaceAll (s, ">", "&gt;");
return (s)};
case charset {
soap.constants.charsetLatin1;
soap.constants.charsetUsAscii {
adrdocument = soap.xmlutils.createRequest (adrrequest, fixEntities (string (adrRequestText^)))};
soap.constants.charsetUtf8;
soap.constants.charsetUtf16 {
if defined (string.utf16ToAnsi) {
local (xmltext);
if charset == soap.constants.charsetUtf8 {
xmltext = string.utf8ToAnsi (string (adrRequestText^))}
else { //utf-16
xmltext = string.utf16ToAnsi (string (adrRequestText^))};
xmltext = fixEntities (xmltext);
adrdocument = soap.xmlutils.createRequest (adrrequest, xmltext)}
else {
soapfault (soap.constants.faultClient, "Can't decode the request because \"" + charset + "\" is not a supported character encoding.")}}}
else {
soapfault (soap.constants.faultClient, "Can't decode the request because \"" + charset + "\" is not a supported character encoding.")}}
else {
soapfault (soap.constants.faultClient, tryerror)}};
if not flfault { //locate envelope
try {
adrenvelope = soap.xmlutils.getFirstChildElement (adrdocument);
soap.xmlutils.pushScope (adrrequest, adrenvelope)}
else {
soapfault (soap.constants.faultClient, tryerror)}};
if not flfault { //version check
if not soap.xmlutils.elementMatches (adrrequest, adrenvelope, @soap.qname.envelope) {
soapfault (soap.constants.faultVersionMismatch, "Can't process the request because the Envelope could not be located.")}};
if not flfault { //locate header, body, and others
try {
local (adrtemp = soap.xmlutils.getFirstChildElement (adrenvelope));
soap.xmlutils.pushScope (adrrequest, adrtemp);
if soap.xmlutils.elementMatches (adrrequest, adrtemp, @soap.qname.header) {
adrheader = adrtemp;
soap.xmlutils.popScope (adrrequest);
adrtemp = soap.xmlutils.getNextSiblingElement (adrtemp);
soap.xmlutils.pushScope (adrrequest, adrtemp)};
if soap.xmlutils.elementMatches (adrrequest, adrtemp, @soap.qname.body) {
adrbody = adrtemp;
adrothers = soap.xmlutils.getNextSiblingElement (adrtemp);
if soap.xmlutils.elementMatches (adrrequest, adrothers, @soap.qname.body) { //reject requests with multiple <Body> elements.
soapFault (soap.constants.faultServer, "Can't process the request because it contains multiple Body elements.")};
if soap.xmlutils.elementMatches (adrrequest, adrothers, @soap.qname.header) { //reject requests with <Header> after <Body>.
soapFault (soap.constants.faultServer, "Can't process the request because it contains a Header element after the Body element.")}}
else {
soapfault (soap.constants.faultClient, "Can't process the request because the Body could not be located.")}}
else {
soapfault (soap.constants.faultClient, tryerror)}};
if not flfault { //look for mustUnderstand and actor attributes in header entries
if adrheader {
local (nomad = soap.xmlutils.getFirstChildElement (adrheader));
while nomad {
local (mustUnderstand = soap.xmlutils.getAttributeValue (nomad, soap.xmlutils.setQualifiedName (adrrequest, "mustUnderstand", soap.constants.nsEnvelopeURI)));
case mustUnderstand {
"1" {
soapfault (soap.constants.faultMustUnderstand, "Can't process the request because this implementation does not support the mustUnderstand attribute.");
break};
"0";
false {
}}//do nothing
else { //it's not a valid value -- fault
soapfault (soap.constants.faultClient, "Can't process the request because a Header element has a mustUnderstand attribute with an illegal value.")};
local (actor = soap.xmlutils.getAttributeValue (nomad, soap.xmlutils.setQualifiedName (adrrequest, "actor", soap.constants.nsEnvelopeURI)));
if actor != false {
if actor != soap.constants.actorNext {
soapfault (soap.constants.faultServer, "Can't process the request because this implementation does not support the actor attribute.")}};
nomad = soap.xmlutils.getNextSiblingElement (nomad)}}};
if not flfault { //locate method call
try {
adrmethodcall = soap.xmlutils.getFirstChildElement (adrbody);
methodname = xml.convertToDisplayName (nameOf (adrmethodcall^));
bundle { //make sure methodName is valid
on methodNameError () {
scriptError ("Can't encode the SOAP request because '" + methodName + "' is not a valid method name.")};
local (ch = methodName[1]);
if not string.isAlpha (ch) {
if (ch != '_') and (ch != ':') {
methodNameError ()}};
local (i, ct = sizeOf (methodName));
on isValidNameChar (ch) {
if string.isAlpha (ch) {
return (true)};
if string.isNumeric (ch) {
return (true)};
case ch {
'.';
'-';
'_';
':' {
return (true)}};
return (false)};
if ct > 1 {
for i = 2 to ct {
if not isValidNameChar (methodName[i]) {
methodNameError ()}}}};
responsename = methodname + "Response"}
else {
soapfault (soap.constants.faultClient, tryerror)}};
if not flfault { //decode the method call struct
try {
soap.xmlutils.pushScope (adrrequest, adrmethodcall);
local (adritem = soap.xmlutils.getFirstChildElement (adrmethodcall));
while (adritem) {
local (paramname = xml.convertToDisplayName (nameOf (adritem^)));
paramrecord = paramrecord + {paramname:soap.decode.main (adrrequest, adritem)};
adritem = soap.xmlutils.getNextSiblingElement (adritem)}}
else {
soapfault (soap.constants.faultClient, tryerror)}}};
bundle { //locate and call the handler
local (adrhandler, securitylist = {});
if not flfault { //locate the script and collect addresses of the #security scripts encountered on the path
try {
local (name, s = actionURI);
if s beginsWith "http://" { //03/28/2001 JES: allow for full URLs in the actionURI/SOAPAction header
if not (s endsWith "/") {
s = s + "/"};
local (urlParts = string.urlSplit (s));
s = string.innerCaseName (string.replaceAll (urlParts[2], ".", " "));
if s endsWith "/" {
s = string.mid (s, 1, sizeOf (s) - 1)};
s = s + "/" + urlParts[3]};
if s endsWith "/" {
s = string.mid (s, 1, sizeOf (s) - 1)};
if string.lower (string.nthField (s, '/', string.countFields (s, '/'))) != string.lower (methodname) {
s = s + "/" + methodname};
s = string.popLeading (s, '/');
local (nomad = @user.soap.rpcHandlers);
loop {
if typeOf (nomad^) == tabletype {
local (adr = @nomad^.["#security"]);
if defined (adr^) {
securitylist = securitylist + {adr}}};
if sizeof (s) == 0 {
break};
name = string.nthField (s, '/', 1);
nomad = @nomad^.[name];
if typeof (nomad^) == addresstype {
nomad = nomad^};
s = string.delete (s, 1, sizeof (name) + 1)};
adrhandler = nomad}
else {
soapfault (soap.constants.faultClient, tryError)}};
if not flfault { //check in with the #security scripts
try {
local (adr);
for adr in securitylist {
callScript (string (adr), {}, adrcontext)}}
else {
soapfault (soap.constants.faultClient, tryError)}};
if not flfault { //call handler: the actionURI defines the table containing the script, the methodname defines the name of the script in that table
try {
adrrequest^.client = client; //11/21/00 JES: make sure the handler has the client and the responder name in scope
adrrequest^.responder = responder;
adrrequest^.adrRequest = adrrequest; //03/17/2001 JES: give the RPC handler access to the request address
returnvalue = callscript (adrhandler, paramrecord, adrrequest)}
else {
local (errormessage = tryerror);
if sizeOf (paramrecord) > 0 { //check for parameter name mismatches
if tryError contains " because it doesnŐt define a parameter named " {
local (flParamNameError = false, ct = sizeOf (paramrecord));
if tryError contains "param1" {
errormessage = "Can't call the method \"" + nameOf (adrhandler^) + "\" because the input parameters don't match the expected parameters. If the client is Frontier/Radio, use a record of name/value pairs for the input parameters."}
else {
for i = 1 to ct {
if tryError contains nameOf (paramrecord[i]) {
flParamNameError = true;
errormessage = "Can't call the method \"" + nameOf (adrhandler^) + "\" because it doesn't take a parameter named \"" + nameOf (paramrecord[i]) + "\".";
break}}}}};
soapfault (soap.constants.faultClient, errormessage)}}};
bundle { //encode response
local (adrdocument, adrenvelope, adrbody, adrmethodresponse, adrreturnvalue);
if not flfault { //build document tree
try {
adrdocument = soap.xmlutils.createResponse (adrresponse);
adrenvelope = soap.xmlutils.addElement (adrdocument, "Envelope");
soap.xmlutils.setNamespacePrefixOfElement (adrenvelope, soap.constants.nsEnvelopePrefix);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsEnvelopePrefix, soap.constants.nsEnvelopeURI);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsEncodingPrefix, soap.constants.nsEncodingURI);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsSchemaPrefix, soap.constants.nsSchemaURI);
soap.xmlutils.declareNamespaceInElement (adrenvelope, soap.constants.nsSchemaDataPrefix, soap.constants.nsSchemaDataURI);
soap.xmlutils.addAttributeValue (adrenvelope, soap.constants.nsEnvelopePrefix +":" + "encodingStyle", soap.constants.nsEncodingURI);
adrbody = soap.xmlutils.addElement (adrenvelope, "Body");
soap.xmlutils.setNamespacePrefixOfElement (adrbody, soap.constants.nsEnvelopePrefix);
adrmethodresponse = soap.xmlutils.addElement (adrbody, responsename)}
else {
soapfault (soap.constants.faultServer, tryerror)}};
if not flfault { //encode return value
try {
local (customStructType, customNamespace, customNamespaceURI);
bundle { //pick up data needed for method-defined array types and namespaces
if defined (adrrequest^.customStructType) {
customStructType = adrrequest^.customStructType};
if defined (adrrequest^.customNamespace) {
customNamespace = adrrequest^.customNamespace};
if defined (adrrequest^.customNamespaceURI) {
customNamespaceURI = adrrequest^.customNamespaceURI}};
if sizeOf (returnvalue) == 0 and getBinaryType (returnvalue) == unknownType {
new (tableType, @adrmethodresponse^.["/atts"])}
else {
soap.encode.main (adrresponse, returnvalue, adrmethodresponse, "Result", customStructType, customNamespace, customNamespaceURI)}}
else {
soapfault (soap.constants.faultServer, tryerror)}};
if not flfault { //decompile document tree into XML text
try {
case charset {
soap.constants.charsetLatin1;
soap.constants.charsetUsAscii {
adrResponseText^ = xml.decompile (adrdocument)};
soap.constants.charsetUtf8;
soap.constants.charsetUtf16 {
if charset == soap.constants.charsetUtf8 {
adrResponseText^ = string.ansiToUtf8 (xml.decompile (adrdocument))}
else { //utf-16
adrResponseText^ = string.ansiToUtf16 (xml.decompile (adrdocument))}}};
responseHeaders.["Content-Type"] = "text/xml; charset=\"" + string.lower (charset) + "\""}
else {
soapfault (soap.constants.faultServer, tryerror)}}};
return (not flfault)};
<<bundle //test code
<<local (actionURI = "/validator1")
<<local (requesttext = string (scratchpad.soapRequest))
<<local (responsetext)
<<server (actionURI, @requesttext, @responsetext)
<<op.newOutlineObject (responsetext, @scratchpad.soapResponse)
<<edit (@scratchpad.soapResponse)
This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.