From 3e60b07ef3168f08e9429462f419a98be5637714 Mon Sep 17 00:00:00 2001 From: adam Date: Thu, 8 Jul 2004 10:03:20 +0000 Subject: [PATCH] initial import darcs-hash:20040708100320-5007d-7ef95c19edc6e534236a022f53ff690a42131530.gz --- src/org/ibex/net/HTTP.java | 1303 ++++++++++++++++++++++++++++++++++++++++++ src/org/ibex/net/SOAP.java | 282 +++++++++ src/org/ibex/net/XMLRPC.java | 348 +++++++++++ 3 files changed, 1933 insertions(+) create mode 100644 src/org/ibex/net/HTTP.java create mode 100644 src/org/ibex/net/SOAP.java create mode 100644 src/org/ibex/net/XMLRPC.java diff --git a/src/org/ibex/net/HTTP.java b/src/org/ibex/net/HTTP.java new file mode 100644 index 0000000..15ba899 --- /dev/null +++ b/src/org/ibex/net/HTTP.java @@ -0,0 +1,1303 @@ +// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] +package org.ibex.net; + +import java.net.*; +import java.io.*; +import java.util.*; +import org.ibex.js.*; +import org.ibex.util.*; +import org.ibex.plat.*; +import org.ibex.core.*; +import org.ibex.crypto.*; + +/** + * This object encapsulates a *single* HTTP connection. Multiple requests may be pipelined over a connection (thread-safe), + * although any IOException encountered in a request will invalidate all later requests. + */ +public class HTTP { + + + // Public Methods //////////////////////////////////////////////////////////////////////////////////////// + + public HTTP(String url) { this(url, false); } + public HTTP(String url, boolean skipResolveCheck) { originalUrl = url; this.skipResolveCheck = skipResolveCheck; } + + /** Performs an HTTP GET request */ + public InputStream GET() throws IOException { return makeRequest(null, null); } + + /** Performs an HTTP POST request; content is additional headers, blank line, and body */ + public InputStream POST(String contentType, String content) throws IOException { return makeRequest(contentType, content); } + + public static class HTTPException extends IOException { public HTTPException(String s) { super(s); } } + + public static HTTP stdio = new HTTP("stdio:"); + + + // Statics /////////////////////////////////////////////////////////////////////////////////////////////// + + static Hash resolvedHosts = new Hash(); ///< cache for resolveAndCheckIfFirewalled() + private static Hash authCache = new Hash(); ///< cache of userInfo strings, keyed on originalUrl + + + // Instance Data /////////////////////////////////////////////////////////////////////////////////////////////// + + final String originalUrl; ///< the URL as passed to the original constructor; this is never changed + String url = null; ///< the URL to connect to; this is munged when the url is parsed */ + String host = null; ///< the host to connect to + int port = -1; ///< the port to connect on + boolean ssl = false; ///< true if SSL (HTTPS) should be used + String path = null; ///< the path (URI) to retrieve on the server + Socket sock = null; ///< the socket + InputStream in = null; ///< the socket's inputstream + String userInfo = null; ///< the username and password portions of the URL + boolean firstRequest = true; ///< true iff this is the first request to be made on this socket + boolean skipResolveCheck = false; ///< allowed to skip the resolve check when downloading PAC script + boolean proxied = false; ///< true iff we're using a proxy + + /** this is null if the current request is the first request on + * this HTTP connection; otherwise it is a Semaphore which will be + * released once the request ahead of us has recieved its response + */ + Semaphore okToRecieve = null; + + /** + * This method isn't synchronized; however, only one thread can be in the inner synchronized block at a time, and the rest of + * the method is protected by in-order one-at-a-time semaphore lock-steps + */ + private InputStream makeRequest(String contentType, String content) throws IOException { + + // Step 1: send the request and establish a semaphore to stop any requests that pipeline after us + Semaphore blockOn = null; + Semaphore releaseMe = null; + synchronized(this) { + try { + connect(); + sendRequest(contentType, content); + } catch (IOException e) { + reset(); + throw e; + } + blockOn = okToRecieve; + releaseMe = okToRecieve = new Semaphore(); + } + + // Step 2: wait for requests ahead of us to complete, then read the reply off the stream + boolean doRelease = true; + try { + if (blockOn != null) blockOn.block(); + + // previous call wrecked the socket connection, but we already sent our request, so we can't just retry -- + // this could cause the server to receive the request twice, which could be bad (think of the case where the + // server call causes Amazon.com to ship you an item with one-click purchasing). + if (in == null) + throw new HTTPException("a previous pipelined call messed up the socket"); + + Hashtable h = in == null ? null : parseHeaders(in); + if (h == null) { + if (firstRequest) throw new HTTPException("server closed the socket with no response"); + // sometimes the server chooses to close the stream between requests + reset(); + releaseMe.release(); + return makeRequest(contentType, content); + } + + String reply = h.get("STATUSLINE").toString(); + + if (reply.startsWith("407") || reply.startsWith("401")) { + + if (reply.startsWith("407")) doProxyAuth(h, content == null ? "GET" : "POST"); + else doWebAuth(h, content == null ? "GET" : "POST"); + + if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) { + if (Log.on) Log.info(this, "proxy returned an HTTP/1.0 reply with no content-length..."); + reset(); + } else { + int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString()); + new HTTPInputStream(in, cl, releaseMe).close(); + } + releaseMe.release(); + return makeRequest(contentType, content); + + } else if (reply.startsWith("2")) { + if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) + throw new HTTPException("Ibex does not support HTTP/1.0 servers which fail to return the Content-Length header"); + int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString()); + InputStream ret = new HTTPInputStream(in, cl, releaseMe); + if ("gzip".equals(h.get("content-encoding"))) ret = new java.util.zip.GZIPInputStream(ret); + doRelease = false; + return ret; + + } else { + throw new HTTPException("HTTP Error: " + reply); + + } + + } catch (IOException e) { reset(); throw e; + } finally { if (doRelease) releaseMe.release(); + } + } + + + // Safeguarded DNS Resolver /////////////////////////////////////////////////////////////////////////// + + /** + * resolves the hostname and returns it as a string in the form "x.y.z.w" + * @throws HTTPException if the host falls within a firewalled netblock + */ + private void resolveAndCheckIfFirewalled(String host) throws HTTPException { + + // cached + if (resolvedHosts.get(host) != null) return; + + // if all scripts are trustworthy (local FS), continue + if (Main.originAddr == null) return; + + // resolve using DNS + try { + InetAddress addr = InetAddress.getByName(host); + byte[] quadbyte = addr.getAddress(); + if ((quadbyte[0] == 10 || + (quadbyte[0] == 192 && quadbyte[1] == 168) || + (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(Main.originAddr)) + throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() + + "] is in a firewalled netblock"); + return; + } catch (UnknownHostException uhe) { } + + if (Platform.detectProxy() == null) + throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured"); + } + + + // Methods to attempt socket creation ///////////////////////////////////////////////////////////////// + + private Socket getSocket(String host, int port, boolean ssl, boolean negotiate) throws IOException { + Socket ret = ssl ? new SSL(host, port, negotiate) : new Socket(java.net.InetAddress.getByName(host), port); + ret.setTcpNoDelay(true); + return ret; + } + + /** Attempts a direct connection */ + private Socket attemptDirect() { + try { + Log.info(this, "attempting to create unproxied socket to " + + host + ":" + port + (ssl ? " [ssl]" : "")); + return getSocket(host, port, ssl, true); + } catch (IOException e) { + if (Log.on) Log.info(this, "exception in attemptDirect(): " + e); + return null; + } + } + + /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */ + private Socket attemptHttpProxy(String proxyHost, int proxyPort) { + try { + if (Log.verbose) Log.info(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort); + Socket sock = getSocket(proxyHost, proxyPort, ssl, false); + + if (!ssl) { + if (!path.startsWith("http://")) path = "http://" + host + ":" + port + path; + return sock; + } + + PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); + BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream())); + pw.print("CONNECT " + host + ":" + port + " HTTP/1.1\r\n\r\n"); + pw.flush(); + String s = br.readLine(); + if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\""); + while (br.readLine().length() > 0) { }; + ((SSL)sock).negotiate(); + return sock; + + } catch (IOException e) { + if (Log.on) Log.info(this, "exception in attemptHttpProxy(): " + e); + return null; + } + } + + /** + * Implements SOCKSv4 with v4a DNS extension + * @see http://www.socks.nec.com/protocol/socks4.protocol + * @see http://www.socks.nec.com/protocol/socks4a.protocol + */ + private Socket attemptSocksProxy(String proxyHost, int proxyPort) { + + // even if host is already a "x.y.z.w" string, we use this to parse it into bytes + InetAddress addr = null; + try { addr = InetAddress.getByName(host); } catch (Exception e) { } + + if (Log.verbose) Log.info(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") + + " proxied socket using proxy " + proxyHost + ":" + proxyPort); + + try { + Socket sock = getSocket(proxyHost, proxyPort, ssl, false); + + DataOutputStream dos = new DataOutputStream(sock.getOutputStream()); + dos.writeByte(0x04); // SOCKSv4(a) + dos.writeByte(0x01); // CONNECT + dos.writeShort(port & 0xffff); // port + if (addr == null) dos.writeInt(0x00000001); // bogus IP + else dos.write(addr.getAddress()); // actual IP + dos.writeByte(0x00); // no userid + if (addr == null) { + PrintWriter pw = new PrintWriter(new OutputStreamWriter(dos)); + pw.print(host); + pw.flush(); + dos.writeByte(0x00); // hostname null terminator + } + dos.flush(); + + DataInputStream dis = new DataInputStream(sock.getInputStream()); + dis.readByte(); // reply version + byte success = dis.readByte(); // success/fail + dis.skip(6); // ip/port + + if ((int)(success & 0xff) == 90) { + if (ssl) ((SSL)sock).negotiate(); + return sock; + } + if (Log.on) Log.info(this, "SOCKS server denied access, code " + (success & 0xff)); + return null; + + } catch (IOException e) { + if (Log.on) Log.info(this, "exception in attemptSocksProxy(): " + e); + return null; + } + } + + /** executes the PAC script and dispatches a call to one of the other attempt methods based on the result */ + private Socket attemptPAC(org.ibex.js.JS pacFunc) { + if (Log.verbose) Log.info(this, "evaluating PAC script"); + String pac = null; + try { + Object obj = pacFunc.call(url, host, null, null, 2); + if (Log.verbose) Log.info(this, " PAC script returned \"" + obj + "\""); + pac = obj.toString(); + } catch (Throwable e) { + if (Log.on) Log.info(this, "PAC script threw exception " + e); + return null; + } + + StringTokenizer st = new StringTokenizer(pac, ";", false); + while (st.hasMoreTokens()) { + String token = st.nextToken().trim(); + if (Log.verbose) Log.info(this, " trying \"" + token + "\"..."); + try { + Socket ret = null; + if (token.startsWith("DIRECT")) + ret = attemptDirect(); + else if (token.startsWith("PROXY")) + ret = attemptHttpProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')), + Integer.parseInt(token.substring(token.indexOf(':') + 1))); + else if (token.startsWith("SOCKS")) + ret = attemptSocksProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')), + Integer.parseInt(token.substring(token.indexOf(':') + 1))); + if (ret != null) return ret; + } catch (Throwable e) { + if (Log.on) Log.info(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token"); + } + } + if (Log.on) Log.info(this, "all PAC results exhausted"); + return null; + } + + + // Everything Else //////////////////////////////////////////////////////////////////////////// + + private synchronized void connect() throws IOException { + if (originalUrl.equals("stdio:")) { in = new BufferedInputStream(System.in); return; } + if (sock != null) { + if (in == null) in = new BufferedInputStream(sock.getInputStream()); + return; + } + // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo() + String url = originalUrl; + userInfo = url.substring(url.indexOf("://") + 3); + userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/')); + if (userInfo.indexOf('@') != -1) { + userInfo = userInfo.substring(0, userInfo.indexOf('@')); + url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1); + } else { + userInfo = null; + } + + if (url.startsWith("https:")) { + ssl = true; + } else if (!url.startsWith("http:")) { + throw new IOException("HTTP only supports http/https urls"); + } + if (url.indexOf("://") == -1) throw new IOException("URLs must contain a ://"); + String temphost = url.substring(url.indexOf("://") + 3); + path = temphost.substring(temphost.indexOf('/')); + temphost = temphost.substring(0, temphost.indexOf('/')); + if (temphost.indexOf(':') != -1) { + port = Integer.parseInt(temphost.substring(temphost.indexOf(':')+1)); + temphost = temphost.substring(0, temphost.indexOf(':')); + } else { + port = ssl ? 443 : 80; + } + if (!skipResolveCheck) resolveAndCheckIfFirewalled(temphost); + host = temphost; + if (Log.verbose) Log.info(this, "creating HTTP object for connection to " + host + ":" + port); + + Proxy pi = Platform.detectProxy(); + OUTER: do { + if (pi != null) { + for(int i=0; i length) len = length; + int ret = b == null ? (int)super.skip(len) : super.read(b, off, len); + if (ret >= 0) { + length -= ret; + good = true; + } + return ret; + } finally { + if (!good) reset(); + } + } + + public void close() throws IOException { + if (contentLength == -1) { + while(!chunkedDone) { + if (length != 0) skip(length); + readChunk(); + } + skip(2); + } else { + if (length != 0) skip(length); + } + if (releaseMe != null) releaseMe.release(); + } + } + + void reset() { + firstRequest = true; + in = null; + sock = null; + } + + + // Misc Helpers /////////////////////////////////////////////////////////////////////////////////// + + /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */ + private Hashtable parseHeaders(InputStream in) throws IOException { + Hashtable ret = new Hashtable(); + + // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers + byte[] buf = new byte[4096]; + int buflen = 0; + while(true) { + int read = in.read(); + if (read == -1 && buflen == 0) return null; + if (read == -1) throw new HTTPException("stream closed while reading headers"); + buf[buflen++] = (byte)read; + if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && + buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') + break; + if (buflen >=2 && buf[buflen - 1] == '\n' && buf[buflen - 2] == '\n') + break; // nice for people using stdio + if (buflen == buf.length) { + byte[] newbuf = new byte[buf.length * 2]; + System.arraycopy(buf, 0, newbuf, 0, buflen); + buf = newbuf; + } + } + + BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen))); + String s = br.readLine(); + if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s); + ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1)); + ret.put("HTTP", s.substring(5, s.indexOf(' '))); + + while((s = br.readLine()) != null && s.length() > 0) { + String front = s.substring(0, s.indexOf(':')).toLowerCase(); + String back = s.substring(s.indexOf(':') + 1).trim(); + // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www) + if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue; + ret.put(front, back); + } + return ret; + } + + private Hashtable parseAuthenticationChallenge(String s) { + Hashtable ret = new Hashtable(); + + s = s.trim(); + ret.put("AUTHTYPE", s.substring(0, s.indexOf(' '))); + s = s.substring(s.indexOf(' ')).trim(); + + while (s.length() > 0) { + String val = null; + String key = s.substring(0, s.indexOf('=')); + s = s.substring(s.indexOf('=') + 1); + if (s.charAt(0) == '\"') { + s = s.substring(1); + val = s.substring(0, s.indexOf('\"')); + s = s.substring(s.indexOf('\"') + 1); + } else { + val = s.indexOf(',') == -1 ? s : s.substring(0, s.indexOf(',')); + s = s.indexOf(',') == -1 ? "" : s.substring(s.indexOf(',') + 1); + } + if (s.length() > 0 && s.charAt(0) == ',') s = s.substring(1); + s = s.trim(); + ret.put(key, val); + } + return ret; + } + + private String H(String s) throws IOException { + byte[] b = s.getBytes("UTF8"); + MD5 md5 = new MD5(); + md5.update(b, 0, b.length); + byte[] out = new byte[md5.getDigestSize()]; + md5.doFinal(out, 0); + String ret = ""; + for(int i=0; i> 4); + ret += "0123456789abcdef".charAt(out[i] & 0x0f); + } + return ret; + } + + + // Proxy /////////////////////////////////////////////////////////// + + /** encapsulates most of the proxy logic; some is shared in HTTP.java */ + public static class Proxy { + + public String httpProxyHost = null; ///< the HTTP Proxy host to use + public int httpProxyPort = -1; ///< the HTTP Proxy port to use + public String httpsProxyHost = null; ///< seperate proxy for HTTPS + public int httpsProxyPort = -1; + public String socksProxyHost = null; ///< the SOCKS Proxy Host to use + public int socksProxyPort = -1; ///< the SOCKS Proxy Port to use + public String[] excluded = new String[] { }; ///< hosts to be excluded from proxy use; wildcards permitted + public JS proxyAutoConfigFunction = null; ///< the PAC script + + public static Proxy detectProxyViaManual() { + Proxy ret = new Proxy(); + + ret.httpProxyHost = Platform.getEnv("http_proxy"); + if (ret.httpProxyHost != null) { + if (ret.httpProxyHost.startsWith("http://")) ret.httpProxyHost = ret.httpProxyHost.substring(7); + if (ret.httpProxyHost.endsWith("/")) + ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.length() - 1); + if (ret.httpProxyHost.indexOf(':') != -1) { + ret.httpProxyPort = Integer.parseInt(ret.httpProxyHost.substring(ret.httpProxyHost.indexOf(':') + 1)); + ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.indexOf(':')); + } else { + ret.httpProxyPort = 80; + } + } + + ret.httpsProxyHost = Platform.getEnv("https_proxy"); + if (ret.httpsProxyHost != null) { + if (ret.httpsProxyHost.startsWith("https://")) ret.httpsProxyHost = ret.httpsProxyHost.substring(7); + if (ret.httpsProxyHost.endsWith("/")) + ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.length() - 1); + if (ret.httpsProxyHost.indexOf(':') != -1) { + ret.httpsProxyPort = Integer.parseInt(ret.httpsProxyHost.substring(ret.httpsProxyHost.indexOf(':') + 1)); + ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.indexOf(':')); + } else { + ret.httpsProxyPort = 80; + } + } + + ret.socksProxyHost = Platform.getEnv("socks_proxy"); + if (ret.socksProxyHost != null) { + if (ret.socksProxyHost.startsWith("socks://")) ret.socksProxyHost = ret.socksProxyHost.substring(7); + if (ret.socksProxyHost.endsWith("/")) + ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.length() - 1); + if (ret.socksProxyHost.indexOf(':') != -1) { + ret.socksProxyPort = Integer.parseInt(ret.socksProxyHost.substring(ret.socksProxyHost.indexOf(':') + 1)); + ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.indexOf(':')); + } else { + ret.socksProxyPort = 80; + } + } + + String noproxy = Platform.getEnv("no_proxy"); + if (noproxy != null) { + StringTokenizer st = new StringTokenizer(noproxy, ","); + ret.excluded = new String[st.countTokens()]; + for(int i=0; st.hasMoreTokens(); i++) ret.excluded[i] = st.nextToken(); + } + + if (ret.httpProxyHost == null && ret.socksProxyHost == null) return null; + return ret; + } + + public static JSScope proxyAutoConfigRootScope = new ProxyAutoConfigRootScope(); + public static JS getProxyAutoConfigFunction(String url) { + try { + BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url, true).GET())); + String s = null; + String script = ""; + while((s = br.readLine()) != null) script += s + "\n"; + if (Log.on) Log.info(Proxy.class, "successfully retrieved WPAD PAC:"); + if (Log.on) Log.info(Proxy.class, script); + + // MS CARP hack + Vector carpHosts = new Vector(); + for(int i=0; i= d1 && day <= d2) || (d1 > d2 && (day >= d1 || day <= d2))) ? T : F; + + case "dateRange": throw new JSExn("Ibex does not support dateRange() in PAC scripts"); + case "timeRange": throw new JSExn("Ibex does not support timeRange() in PAC scripts"); + //#end + return super.callMethod(method, a0, a1, a2, rest, nargs); + } + private static boolean match(String[] arr, String s, int index) { + if (index >= arr.length) return true; + for(int i=0; i epoch. + time *= 10000; // tenths of a microsecond. + // convert to little-endian byte array. + byte[] timestamp = new byte[8]; + for (int i = 0; i < 8; i++) { + timestamp[i] = (byte) time; + time >>>= 8; + } + byte[] blob = new byte[blobSignature.length + reserved.length + + timestamp.length + clientChallenge.length + + unknown1.length + targetInformation.length + + unknown2.length]; + int offset = 0; + System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length); + offset += blobSignature.length; + System.arraycopy(reserved, 0, blob, offset, reserved.length); + offset += reserved.length; + System.arraycopy(timestamp, 0, blob, offset, timestamp.length); + offset += timestamp.length; + System.arraycopy(clientChallenge, 0, blob, offset, + clientChallenge.length); + offset += clientChallenge.length; + System.arraycopy(unknown1, 0, blob, offset, unknown1.length); + offset += unknown1.length; + System.arraycopy(targetInformation, 0, blob, offset, + targetInformation.length); + offset += targetInformation.length; + System.arraycopy(unknown2, 0, blob, offset, unknown2.length); + return blob; + } + + /** + * Calculates the HMAC-MD5 hash of the given data using the specified + * hashing key. + * + * @param data The data for which the hash will be calculated. + * @param key The hashing key. + * + * @return The HMAC-MD5 hash of the given data. + */ + private static byte[] hmacMD5(byte[] data, byte[] key) { + byte[] ipad = new byte[64]; + byte[] opad = new byte[64]; + for (int i = 0; i < 64; i++) { + ipad[i] = (byte) 0x36; + opad[i] = (byte) 0x5c; + } + for (int i = key.length - 1; i >= 0; i--) { + ipad[i] ^= key[i]; + opad[i] ^= key[i]; + } + byte[] content = new byte[data.length + 64]; + System.arraycopy(ipad, 0, content, 0, 64); + System.arraycopy(data, 0, content, 64, data.length); + MD5 md5 = new MD5(); + md5.update(content, 0, content.length); + data = new byte[md5.getDigestSize()]; + md5.doFinal(data, 0); + content = new byte[data.length + 64]; + System.arraycopy(opad, 0, content, 0, 64); + System.arraycopy(data, 0, content, 64, data.length); + md5 = new MD5(); + md5.update(content, 0, content.length); + byte[] ret = new byte[md5.getDigestSize()]; + md5.doFinal(ret, 0); + return ret; + } + + /** + * Creates a DES encryption key from the given key material. + * + * @param bytes A byte array containing the DES key material. + * @param offset The offset in the given byte array at which + * the 7-byte key material starts. + * + * @return A DES encryption key created from the key material + * starting at the specified offset in the given byte array. + */ + /* + private static Key createDESKey(byte[] bytes, int offset) { + byte[] keyBytes = new byte[7]; + System.arraycopy(bytes, offset, keyBytes, 0, 7); + byte[] material = new byte[8]; + material[0] = keyBytes[0]; + material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1); + material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2); + material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3); + material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4); + material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5); + material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6); + material[7] = (byte) (keyBytes[6] << 1); + oddParity(material); + return new SecretKeySpec(material, "DES"); + } + */ + + /** + * Applies odd parity to the given byte array. + * + * @param bytes The data whose parity bits are to be adjusted for + * odd parity. + */ + private static void oddParity(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ + (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^ + (b >>> 1)) & 0x01) == 0; + if (needsParity) { + bytes[i] |= (byte) 0x01; + } else { + bytes[i] &= (byte) 0xfe; + } + } + } + + } + } +} diff --git a/src/org/ibex/net/SOAP.java b/src/org/ibex/net/SOAP.java new file mode 100644 index 0000000..85670a3 --- /dev/null +++ b/src/org/ibex/net/SOAP.java @@ -0,0 +1,282 @@ +// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] +package org.ibex.net; + +import java.io.*; +import java.util.*; +import org.ibex.js.*; +import org.ibex.util.*; +import org.ibex.crypto.*; + +/** + * A partial RPC-style SOAP 1.1 client. Implemented from the SOAP 1.1 + * Spec and Dave Winer's "SOAP for Busy Developers". This class + * extends XMLRPC in order to share some networking logic. + * + * Currently unsupported features/hacks: + *
  • Multi-ref data and circular references + *
  • 'Document Style' + *
  • WSDL support + *
+ */ +public class SOAP extends XMLRPC { + + /** the desired content of the SOAPAction header */ + String action = null; + + /** the namespace to use */ + String nameSpace = null; + + /** When you get a property from an SOAP, it just returns another SOAP with the property name tacked onto methodname. */ + public Object get(Object name) { + return new SOAP(url, (method.equals("") ? "" : method + ".") + name.toString(), this, action, nameSpace); } + + + // Methods to Recieve and parse SOAP Responses //////////////////////////////////////////////////// + + public void startElement(String name, String[] keys, Object[] vals, int line, int col) { + + content.reset(); + if (name.equals("SOAP-ENV:Envelope")) return; + if (name.equals("SOAP-ENV:Body")) return; + if (name.equals("SOAP-ENV:Fault")) fault = true; + + // add a generic struct; we'll change this if our type is different + objects.addElement(new JS()); + + for(int i=0; i 0 && content.toString().trim().length() > 0) { + + // remove ourselves + Object me = objects.elementAt(objects.size() - 1); + + if (fault || me instanceof String) { + objects.removeElementAt(objects.size() - 1); + objects.addElement(new String(content.getBuf(), 0, content.size()).intern()); + content.reset(); + + } else if (me instanceof byte[]) { + objects.removeElementAt(objects.size() - 1); + objects.addElement(new Stream.ByteArray(Base64.decode(new String(content.getBuf(), 0, content.size())), null)); + content.reset(); + + } else if (me instanceof Integer) { + objects.removeElementAt(objects.size() - 1); + objects.addElement(new Integer(new String(content.getBuf(), 0, content.size()))); + content.reset(); + + } else if (me instanceof Boolean) { + objects.removeElementAt(objects.size() - 1); + String s = new String(content.getBuf(), 0, content.size()).trim(); + if (s.equals("1") || s.equals("true")) objects.addElement(Boolean.TRUE); + else objects.addElement(Boolean.FALSE); + content.reset(); + + } else if (me instanceof Double) { + objects.removeElementAt(objects.size() - 1); + objects.addElement(new Double(new String(content.getBuf(), 0, content.size()))); + content.reset(); + + } else { + // okay, we got PCDATA for what is supposedly a + // struct... somebody's not adding their type info... + String s = new String(content.getBuf(), 0, content.size()).trim(); + boolean hasdot = false; + for(int i=0; i 1 ? objects.elementAt(objects.size() - 2) : null; + + // we want to fold stuff back into the fault object + if (objects.size() < 2) return; + + // our parent "should" be an aggregate type -- add ourselves to it. + if (parent != null && parent instanceof JSArray) { + objects.removeElementAt(objects.size() - 1); + ((JSArray)parent).addElement(me); + + } else if (parent != null && parent instanceof JS) { + objects.removeElementAt(objects.size() - 1); + try { + ((JS)parent).put(name, me); + } catch (JSExn e) { + throw new Error("this should never happen"); + } + + } + + } + + /** Appends the SOAP representation of o to sb */ + void appendObject(String name, Object o, StringBuffer sb) throws JSExn { + if (o instanceof Number) { + if ((double)((Number)o).intValue() == ((Number)o).doubleValue()) { + sb.append(" <" + name + " xsi:type=\"xsd:int\">"); + sb.append(((Number)o).intValue()); + sb.append("\r\n"); + } else { + sb.append(" <" + name + " xsi:type=\"xsd:double\">"); + sb.append(o); + sb.append("\r\n"); + } + + } else if (o instanceof Boolean) { + sb.append(" <" + name + " xsi:type=\"xsd:boolean\">"); + sb.append(((Boolean)o).booleanValue() ? "true" : "false"); + sb.append("\r\n"); + + } else if (o instanceof Stream) { + try { + sb.append(" <" + name + " xsi:type=\"SOAP-ENC:base64\">\r\n"); + InputStream is = ((Stream)o).getInputStream(); + byte[] buf = new byte[54]; + while(true) { + int numread = is.read(buf, 0, 54); + if (numread == -1) break; + byte[] writebuf = buf; + if (numread < buf.length) { + writebuf = new byte[numread]; + System.arraycopy(buf, 0, writebuf, 0, numread); + } + sb.append(" "); + sb.append(new String(Base64.encode(writebuf))); + sb.append("\r\n"); + } + sb.append(((Boolean)o).booleanValue() ? "1" : "0"); + sb.append("\r\n"); + } catch (IOException e) { + if (Log.on) Log.info(this, "caught IOException while attempting to send a ByteStream via SOAP"); + if (Log.on) Log.info(this, e); + throw new JSExn("caught IOException while attempting to send a ByteStream via SOAP"); + } + + } else if (o instanceof String) { + sb.append(" <" + name + " xsi:type=\"xsd:string\">"); + String s = (String)o; + if (s.indexOf('<') == -1 && s.indexOf('&') == -1) { + sb.append(s); + } else { + char[] cbuf = s.toCharArray(); + while(true) { + int oldi = 0, i=0; + while(i < cbuf.length && cbuf[i] != '<' && cbuf[i] != '&') i++; + sb.append(cbuf, oldi, i); + if (i == cbuf.length) break; + if (cbuf[i] == '<') sb.append("<"); + else if (cbuf[i] == '&') sb.append("&"); + i = oldi = i + 1; + } + } + sb.append("\r\n"); + + } else if (o instanceof JSArray) { + JSArray a = (JSArray)o; + sb.append(" <" + name + " SOAP-ENC:arrayType=\"xsd:ur-type[" + a.length() + "]\">"); + for(int i=0; i\r\n"); + + } else if (o instanceof JS) { + JS j = (JS)o; + sb.append(" <" + name + ">"); + Enumeration e = j.keys(); + while(e.hasMoreElements()) { + Object key = e.nextElement(); + appendObject((String)key, j.get(key), sb); + } + sb.append("\r\n"); + + } + } + + protected String buildRequest(JSArray args) throws JSExn, IOException { + // build up the request + StringBuffer content = new StringBuffer(); + content.append("SOAPAction: " + action + "\r\n\r\n"); + content.append("\r\n"); + content.append("\r\n"); + content.append("\r\n"); + content.append(" <"); + content.append(method); + content.append(nameSpace != null ? " xmlns=\"" + nameSpace + "\"" : ""); + content.append(">\r\n"); + if (args.length() > 0) { + Enumeration e = ((JS)args.elementAt(0)).keys(); + while(e.hasMoreElements()) { + Object key = e.nextElement(); + appendObject((String)key, ((JS)args.elementAt(0)).get(key), content); + } + } + content.append(" \r\n"); + return content.toString(); + } + + public SOAP(String url, String methodname, String action, String nameSpace) { + super(url, methodname); + this.action = action; + this.nameSpace = nameSpace; + } + public SOAP(String url, String methodname, SOAP httpSource, String action, String nameSpace) { + super(url, methodname, httpSource); + this.action = action; + this.nameSpace = nameSpace; + } + +} diff --git a/src/org/ibex/net/XMLRPC.java b/src/org/ibex/net/XMLRPC.java new file mode 100644 index 0000000..14aacb5 --- /dev/null +++ b/src/org/ibex/net/XMLRPC.java @@ -0,0 +1,348 @@ +// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] +package org.ibex.net; + +import java.io.*; +import java.util.*; +import org.ibex.js.*; +import org.ibex.util.*; +import org.ibex.crypto.*; + +/** + * An XML-RPC client implemented as a JavaScript Host Object. See the + * Ibex spec for information on its behavior. + * + * NOTE: this client is EXTREMELY lenient in the responses it will + * accept; there are many, many invalid responses that it will + * successfully parse and return. Do NOT use this to determine the + * validity of your server. + * + * This client conforms to The + * XML-RPC Spec, subject to these limitations: + *
    + *
  1. XMLRPC cannot invoke methods that require a argument + *
  2. if a return value contains a , it will be returned as a string + *
  3. The decision to pass a number as or is based + * entirely on whether or not the argument is fractional. Thus, it + * is impossible to pass a non-fractional number to an xmlrpc + * method that insists on being called with a element. We + * hope that most xml-rpc servers will be able to automatically + * convert. + *
+ */ +public class XMLRPC extends JS { + + public XMLRPC(String url, String method) { + this.http = url.startsWith("stdio:") ? HTTP.stdio : new HTTP(url); + this.url = url; + this.method = method; + } + public XMLRPC(String url, String method, XMLRPC httpSource) { + this.http = httpSource.http; this.url = url; this.method = method; } + public Object get(Object name) { + return new XMLRPC(url, (method.equals("") ? "" : method + ".") + name.toString(), this); } + + + /** this holds character content as we read it in -- since there is only one per instance, we don't support mixed content */ + protected AccessibleCharArrayWriter content = new AccessibleCharArrayWriter(100); + protected String url = null; ///< the url to connect to + protected String method = null; ///< the method name to invoke on the remove server + protected HTTP http = null; ///< the HTTP connection to use + private Hash tracker; ///< used to detect multi-ref data + protected boolean fault = false; ///< True iff the return value is a fault (and should be thrown as an exception) + + + /** The object stack. As we process xml elements, pieces of the + * return value are pushed onto and popped off of this stack. + * + * The general protocol is that any time a <value> tag is + * encountered, an empty String ("") is pushed onto the stack. If + * the <value/> node has content (either an anonymous + * string or some other XML node), that content replaces the + * empty string. + * + * If an <array> tag is encountered, a null is pushed onto the + * stack. When a </data> is encountered, we search back on the + * stack to the last null, replace it with a NativeJSArray, and + * insert into it all elements above it on the stack. + * + * If a <struct> tag is encountered, a JSect is pushed + * onto the stack. If a <name> tag is encountered, its CDATA is + * pushed onto the stack. When a </member> is encountered, the + * name (second element on stack) and value (top of stack) are + * popped off the stack and inserted into the struct (third + * element on stack). + */ + protected Vec objects = null; + + + // Recieve //////////////////////////////////////////////////////////////// + + private class Helper extends XML { + public Helper() { super(BUFFER_SIZE); } + + public void startElement(XML.Element c) { + content.reset(); + //#switch(c.getLocalName()) + case "fault": fault = true; + case "struct": objects.setElementAt(new JS(), objects.size() - 1); + case "array": objects.setElementAt(null, objects.size() - 1); + case "value": objects.addElement(""); + //#end + } + + public void endElement(XML.Element c) { + //#switch(c.getLocalName()) + case "int": objects.setElementAt(new Integer(new String(content.getBuf(), 0, content.size())), objects.size() - 1); + case "i4": objects.setElementAt(new Integer(new String(content.getBuf(), 0, content.size())), objects.size() - 1); + case "boolean": objects.setElementAt(content.getBuf()[0] == '1' ? Boolean.TRUE : Boolean.FALSE, objects.size() - 1); + case "string": objects.setElementAt(new String(content.getBuf(), 0, content.size()), objects.size() - 1); + case "double": objects.setElementAt(new Double(new String(content.getBuf(), 0, content.size())), objects.size() - 1); + case "base64": + objects.setElementAt(new Stream.ByteArray(Base64.decode(new String(content.getBuf(), 0, content.size())), + null), objects.size() - 1); + case "name": objects.addElement(new String(content.getBuf(), 0, content.size())); + case "value": if ("".equals(objects.lastElement())) + objects.setElementAt(new String(content.getBuf(), 0, content.size()), objects.size() - 1); + case "dateTime.iso8601": + String s = new String(content.getBuf(), 0, content.size()); + + // strip whitespace + int i=0; + while(Character.isWhitespace(s.charAt(i))) i++; + if (i > 0) s = s.substring(i); + + try { + JSDate nd = new JSDate(); + double date = JSDate.date_msecFromDate(Double.valueOf(s.substring(0, 4)).doubleValue(), + Double.valueOf(s.substring(4, 6)).doubleValue() - 1, + Double.valueOf(s.substring(6, 8)).doubleValue(), + Double.valueOf(s.substring(9, 11)).doubleValue(), + Double.valueOf(s.substring(12, 14)).doubleValue(), + Double.valueOf(s.substring(15, 17)).doubleValue(), + (double)0 + ); + nd.setTime(JSDate.internalUTC(date)); + objects.setElementAt(nd, objects.size() - 1); + + } catch (Exception e) { + throw new RuntimeException("ibex.net.rpc.xml.recieve.malformedDateTag" + + "the server sent a tag which was malformed: " + s); + } + case "member": + Object memberValue = objects.elementAt(objects.size() - 1); + String memberName = (String)objects.elementAt(objects.size() - 2); + JS struct = (JS)objects.elementAt(objects.size() - 3); + try { + struct.put(memberName, memberValue); + } catch (JSExn e) { + throw new Error("this should never happen"); + } + objects.setSize(objects.size() - 2); + case "data": + int i; + for(i=objects.size() - 1; objects.elementAt(i) != null; i--); + JSArray arr = new JSArray(); + try { + for(int j = i + 1; j\n"); + content.append(" \n"); + content.append(" "); + content.append(method); + content.append("\n"); + content.append(" \n"); + for(int i=0; i\n"); + appendObject(args.elementAt(i), content); + content.append(" \n"); + } + content.append(" \n"); + content.append(" "); + return content.toString(); + } + + /** Appends the XML-RPC representation of o to sb */ + void appendObject(Object o, StringBuffer sb) throws JSExn { + + if (o == null) { + throw new JSExn("attempted to send a null value via XML-RPC"); + + } else if (o instanceof Number) { + if ((double)((Number)o).intValue() == ((Number)o).doubleValue()) { + sb.append(" "); + sb.append(((Number)o).intValue()); + sb.append("\n"); + } else { + sb.append(" "); + sb.append(o); + sb.append("\n"); + } + + } else if (o instanceof Boolean) { + sb.append(" "); + sb.append(((Boolean)o).booleanValue() ? "1" : "0"); + sb.append("\n"); + + } else if (o instanceof Stream) { + try { + sb.append(" \n"); + InputStream is = ((Stream)o).getInputStream(); + byte[] buf = new byte[54]; + while(true) { + int numread = is.read(buf, 0, 54); + if (numread == -1) break; + byte[] writebuf = buf; + if (numread < buf.length) { + writebuf = new byte[numread]; + System.arraycopy(buf, 0, writebuf, 0, numread); + } + sb.append(" "); + sb.append(new String(Base64.encode(writebuf))); + sb.append("\n"); + } + sb.append("\n \n"); + } catch (IOException e) { + if (Log.on) Log.info(this, "caught IOException while attempting to send a ByteStream via XML-RPC"); + if (Log.on) Log.info(this, e); + throw new JSExn("caught IOException while attempting to send a ByteStream via XML-RPC"); + } + + } else if (o instanceof String) { + sb.append(" "); + String s = (String)o; + if (s.indexOf('<') == -1 && s.indexOf('&') == -1) { + sb.append(s); + } else { + char[] cbuf = s.toCharArray(); + int oldi = 0, i=0; + while(true) { + while(i < cbuf.length && cbuf[i] != '<' && cbuf[i] != '&') i++; + sb.append(cbuf, oldi, i - oldi); + if (i >= cbuf.length) break; + if (cbuf[i] == '<') sb.append("<"); + else if (cbuf[i] == '&') sb.append("&"); + i = oldi = i + 1; + if (i >= cbuf.length) break; + } + } + sb.append("\n"); + + } else if (o instanceof JSDate) { + sb.append(" "); + java.util.Date d = new java.util.Date(((JSDate)o).getRawTime()); + sb.append(d.getYear() + 1900); + if (d.getMonth() + 1 < 10) sb.append('0'); + sb.append(d.getMonth() + 1); + if (d.getDate() < 10) sb.append('0'); + sb.append(d.getDate()); + sb.append('T'); + if (d.getHours() < 10) sb.append('0'); + sb.append(d.getHours()); + sb.append(':'); + if (d.getMinutes() < 10) sb.append('0'); + sb.append(d.getMinutes()); + sb.append(':'); + if (d.getSeconds() < 10) sb.append('0'); + sb.append(d.getSeconds()); + sb.append("\n"); + + } else if (o instanceof JSArray) { + if (tracker.get(o) != null) throw new JSExn("attempted to send multi-ref data structure via XML-RPC"); + tracker.put(o, Boolean.TRUE); + sb.append(" \n"); + JSArray a = (JSArray)o; + for(int i=0; i\n"); + + } else if (o instanceof JS) { + if (tracker.get(o) != null) throw new JSExn("attempted to send multi-ref data structure via XML-RPC"); + tracker.put(o, Boolean.TRUE); + JS j = (JS)o; + sb.append(" \n"); + Enumeration e = j.keys(); + while(e.hasMoreElements()) { + Object key = e.nextElement(); + sb.append(" " + key + "\n"); + appendObject(j.get(key), sb); + sb.append(" \n"); + } + sb.append(" \n"); + + } else { + throw new JSExn("attempt to send object of type " + o.getClass().getName() + " via XML-RPC"); + + } + } + + + // Call Sequence ////////////////////////////////////////////////////////////////////////// + + public final Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn { + JSArray args = new JSArray(); + for(int i=0; i