// Copyright 2000-2005 the Contributors, as shown in the revision logs. // Licensed under the Apache Public Source License 2.0 ("the License"). // You may not use this file except in compliance with the License. package org.ibex.net; import java.net.*; import java.io.*; import java.util.*; import org.ibex.util.*; 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 static InetAddress originAddr = null; public static String originHost = null; // FIXME: HACK: these shouldn't be set globally public static String userAgent = "Ibex"; public static boolean allowRedirects = true; // Cookies ////////////////////////////////////////////////////////////////////////////// public static class Cookie { public final String name; public final String value; public final String domain; public final String path; public final Date expires; public final boolean secure; public Cookie(String name, String value, String domain, String path, Date expires, boolean secure) { this.name = name; this.value = value; this.domain = domain; this.path = path; this.expires = expires; this.secure = secure; } // FIXME: this could be much more efficient // FIXME currently only implements http://wp.netscape.com/newsref/std/cookie_spec.html public static class Jar { private Hash h = new Hash(); public String getCookieHeader(String domain, String path, boolean secure) { StringBuffer ret = new StringBuffer("Cookie: "); Enumeration e = h.enumerateKeys(); while (e.hasMoreElements()) { Vec v = (Vec)h.get(e.nextElement()); Cookie cookie = null; for(int i=0; i cookie.domain.length())) cookie = c; } if (cookie != null) { ret.append(cookie.name); ret.append("="); ret.append(cookie.value); ret.append("; "); } } //ret.setLength(ret.length() - 2); return ret.toString(); } public void setCookie(String header, String defaultDomain) { String name = null; String value = null; String domain = defaultDomain; String path = "/"; Date expires = null; boolean secure = false; StringTokenizer st = new StringTokenizer(header, ";"); while(st.hasMoreTokens()) { String s = st.nextToken(); if (s.indexOf('=') == -1) { if (s.equals("secure")) secure = true; continue; } String start = s.substring(0, s.indexOf('=')); String end = s.substring(s.indexOf('=')+1); if (name == null) { name = start; value = end; continue; } if (start.toLowerCase().equals("domain")) domain = end; else if (start.toLowerCase().equals("path")) path = end; else if (start.toLowerCase().equals("expires")) expires = new Date(end); } if (h.get(name) == null) h.put(name, new Vec()); ((Vec)h.get(name)).addElement(new Cookie(name, value, domain, path, expires, secure)); } } } // Public Methods //////////////////////////////////////////////////////////////////////////////////////// public HTTP(String url) { this(url, false); } public HTTP(String url, boolean skipResolveCheck) { this(url, skipResolveCheck, null); } public HTTP(String url, Proxy p) { this(url, false, p); } public HTTP(String url, boolean skipResolveCheck, Proxy p) { originalUrl = url; this.skipResolveCheck = skipResolveCheck; proxy = p; } /** Performs an HTTP GET request */ public InputStream GET(String referer, Cookie.Jar cookies) throws IOException { return makeRequest(null, null, referer, cookies); } /** Performs an HTTP POST request; content is additional headers, blank line, and body */ public InputStream POST(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException { return makeRequest(contentType, content, referer, cookies); } 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 Proxy 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, String referer, Cookie.Jar cookies) 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, referer, cookies); } 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, cookies); 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, referer, cookies); } 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, referer, cookies); } else if (reply.startsWith("3") && allowRedirects) { String location = (String)h.get("location"); if (location == null) throw new HTTPException("Got HTTP " + reply.substring(0, 3) + " but no Location header"); Log.info(HTTP.class, "redirecting to " + location); if (content != null) return new HTTP(location).POST(contentType, content, url, cookies); else return new HTTP(location).GET(url, cookies); } 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 (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(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 */ 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; } } // 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); proxied = proxy!=null; if (proxied) sock = proxy.attempt(url, host, this); if (sock==null) { proxied = false; sock = attemptDirect(); } if (sock == null) throw new HTTPException("unable to contact host " + host); if (in == null) in = new BufferedInputStream(sock.getInputStream()); } private void sendRequest(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException { PrintWriter pw = new PrintWriter(new OutputStreamWriter(originalUrl.equals("stdio:") ? System.out : sock.getOutputStream())); if (content != null) { pw.print("POST " + path + " HTTP/1.0\r\n"); // FIXME chunked encoding int contentLength = content.substring(0, 2).equals("\r\n") ? content.length() - 2 : (content.length() - content.indexOf("\r\n\r\n") - 4); pw.print("Content-Length: " + contentLength + "\r\n"); if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n"); } else { pw.print("GET " + path + " HTTP/1.1\r\n"); } if (cookies != null) pw.print(cookies.getCookieHeader(host, path, ssl)); pw.print("User-Agent: " + userAgent + "\r\n"); pw.print("Accept-encoding: gzip\r\n"); pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n"); if (proxied) pw.print("X-RequestOrigin: " + originHost + "\r\n"); if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: "+Proxy.Authorization.authorization2+"\r\n"); if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n"); pw.print(content == null ? "\r\n" : content); pw.print("\r\n"); pw.flush(); } private void doWebAuth(Hashtable h0, String method) throws IOException { if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied"); Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString()); if (h.get("AUTHTYPE").equals("Basic")) { if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected"); authCache.put(originalUrl, "Basic " + new String(Encode.toBase64(userInfo.getBytes("UTF8")))); } else if (h.get("AUTHTYPE").equals("Digest")) { if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale"))) throw new HTTPException("username/password rejected"); String path2 = path; if (path2.startsWith("http://") || path2.startsWith("https://")) { path2 = path2.substring(path2.indexOf("://") + 3); path2 = path2.substring(path2.indexOf('/')); } String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + userInfo.substring(userInfo.indexOf(':') + 1); String A2 = method + ":" + path2; authCache.put(originalUrl, "Digest " + "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " + "realm=\"" + h.get("realm") + "\", " + "nonce=\"" + h.get("nonce") + "\", " + "uri=\"" + path2 + "\", " + (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " + "algorithm=MD5" ); } else { throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE")); } } private void doProxyAuth(Hashtable h0, String method) throws IOException { if (Log.on) Log.info(this, "Proxy AuthChallenge: " + h0.get("proxy-authenticate")); Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString()); String style = h.get("AUTHTYPE").toString(); String realm = (String)h.get("realm"); if (style.equals("NTLM") && Proxy.Authorization.authorization2 == null) { Log.info(this, "Proxy identified itself as NTLM, sending Type 1 packet"); Proxy.Authorization.authorization2 = "NTLM " + Encode.toBase64(Proxy.NTLM.type1); return; } /* if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale"))) Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), Proxy.Authorization.authorization); */ if (style.equals("Basic")) { Proxy.Authorization.authorization2 = "Basic " + new String(Encode.toBase64(Proxy.Authorization.authorization.getBytes("UTF8"))); } else if (style.equals("Digest")) { String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1); String A2 = method + ":" + path; Proxy.Authorization.authorization2 = "Digest " + "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) + "\", " + "realm=\"" + h.get("realm") + "\", " + "nonce=\"" + h.get("nonce") + "\", " + "uri=\"" + path + "\", " + (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " + "algorithm=MD5"; } else if (style.equals("NTLM")) { Log.info(this, "Proxy identified itself as NTLM, got Type 2 packet"); byte[] type2 = Encode.fromBase64(((String)h0.get("proxy-authenticate")).substring(5).trim()); 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, Cookie.Jar cookies) 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; if (front.equals("set-cookie")) cookies.setCookie(back, host); 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 Socket attempt(String url, String host, HTTP http) { for(int i=0; i 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 */ protected Socket attemptSocksProxy(HTTP http, 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(http.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 = http.getSocket(proxyHost, proxyPort, http.ssl, false); DataOutputStream dos = new DataOutputStream(sock.getOutputStream()); dos.writeByte(0x04); // SOCKSv4(a) dos.writeByte(0x01); // CONNECT dos.writeShort(http.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(http.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 (http.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; } } // Authorization /////////////////////////////////////////////////////////////////////////////////// public static class Authorization { static public String authorization = null; static public String authorization2 = null; static public Semaphore waitingForUser = new Semaphore(); // FIXME: temporarily disabled so we can use HTTP outside the core /* public static synchronized void getPassword(final String realm, final String style, final String proxyIP, String oldAuth) throws IOException { // this handles cases where multiple threads hit the proxy auth at the same time -- all but one will block on the // synchronized keyword. If 'authorization' changed while the thread was blocked, it means that the user entered // a password, so we should reattempt authorization. if (authorization != oldAuth) return; if (Log.on) Log.info(Authorization.class, "displaying proxy authorization dialog"); Scheduler.add(new Task() { public void perform() throws IOException, JSExn { Box b = new Box(); Template t = null; // FIXME //Template.buildTemplate("org/ibex/builtin/proxy_authorization.ibex", Stream.getInputStream((JS)Main.builtin.get("org/ibex/builtin/proxy_authorization.ibex")), new Ibex(null)); t.apply(b); b.put("realm", realm); b.put("proxyIP", proxyIP); } }); waitingForUser.block(); if (Log.on) Log.info(Authorization.class, "got proxy authorization info; re-attempting connection"); } */ } /** * An implementation of Microsoft's proprietary NTLM authentication protocol. This code was derived from Eric * Glass's work, and is copyright as follows: * * Copyright (c) 2003 Eric Glass (eglass1 at comcast.net). * * Permission to use, copy, modify, and distribute this document for any purpose and without any fee is hereby * granted, provided that the above copyright notice and this list of conditions appear in all copies. * The most current version of this document may be obtained from http://davenport.sourceforge.net/ntlm.html . */ public static class NTLM { public static final byte[] type1 = new byte[] { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00 }; /** * Calculates the NTLM Response for the given challenge, using the * specified password. * * @param password The user's password. * @param challenge The Type 2 challenge from the server. * * @return The NTLM Response. */ public static byte[] getNTLMResponse(String password, byte[] challenge) throws UnsupportedEncodingException { byte[] ntlmHash = ntlmHash(password); return lmResponse(ntlmHash, challenge); } /** * Calculates the LM Response for the given challenge, using the specified * password. * * @param password The user's password. * @param challenge The Type 2 challenge from the server. * * @return The LM Response. */ public static byte[] getLMResponse(String password, byte[] challenge) { byte[] lmHash = lmHash(password); return lmResponse(lmHash, challenge); } /** * Calculates the NTLMv2 Response for the given challenge, using the * specified authentication target, username, password, target information * block, and client challenge. * * @param target The authentication target (i.e., domain). * @param user The username. * @param password The user's password. * @param targetInformation The target information block from the Type 2 * message. * @param challenge The Type 2 challenge from the server. * @param clientChallenge The random 8-byte client challenge. * * @return The NTLMv2 Response. */ public static byte[] getNTLMv2Response(String target, String user, String password, byte[] targetInformation, byte[] challenge, byte[] clientChallenge) throws UnsupportedEncodingException { byte[] ntlmv2Hash = ntlmv2Hash(target, user, password); byte[] blob = createBlob(targetInformation, clientChallenge); return lmv2Response(ntlmv2Hash, blob, challenge); } /** * Calculates the LMv2 Response for the given challenge, using the * specified authentication target, username, password, and client * challenge. * * @param target The authentication target (i.e., domain). * @param user The username. * @param password The user's password. * @param challenge The Type 2 challenge from the server. * @param clientChallenge The random 8-byte client challenge. * * @return The LMv2 Response. */ public static byte[] getLMv2Response(String target, String user, String password, byte[] challenge, byte[] clientChallenge) throws UnsupportedEncodingException { byte[] ntlmv2Hash = ntlmv2Hash(target, user, password); return lmv2Response(ntlmv2Hash, clientChallenge, challenge); } /** * Calculates the NTLM2 Session Response for the given challenge, using the * specified password and client challenge. * * @param password The user's password. * @param challenge The Type 2 challenge from the server. * @param clientChallenge The random 8-byte client challenge. * * @return The NTLM2 Session Response. This is placed in the NTLM * response field of the Type 3 message; the LM response field contains * the client challenge, null-padded to 24 bytes. */ public static byte[] getNTLM2SessionResponse(String password, byte[] challenge, byte[] clientChallenge) throws UnsupportedEncodingException { byte[] ntlmHash = ntlmHash(password); MD5 md5 = new MD5(); md5.update(challenge, 0, challenge.length); md5.update(clientChallenge, 0, clientChallenge.length); byte[] sessionHash = new byte[8]; byte[] md5_out = new byte[md5.getDigestSize()]; md5.doFinal(md5_out, 0); System.arraycopy(md5_out, 0, sessionHash, 0, 8); return lmResponse(ntlmHash, sessionHash); } /** * Creates the LM Hash of the user's password. * * @param password The password. * * @return The LM Hash of the given password, used in the calculation * of the LM Response. */ private static byte[] lmHash(String password) { /* byte[] oemPassword = password.toUpperCase().getBytes("UTF8"); int length = java.lang.Math.min(oemPassword.length, 14); byte[] keyBytes = new byte[14]; System.arraycopy(oemPassword, 0, keyBytes, 0, length); Key lowKey = createDESKey(keyBytes, 0); Key highKey = createDESKey(keyBytes, 7); byte[] magicConstant = "KGS!@#$%".getBytes("UTF8"); Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); des.init(Cipher.ENCRYPT_MODE, lowKey); byte[] lowHash = des.doFinal(magicConstant); des.init(Cipher.ENCRYPT_MODE, highKey); byte[] highHash = des.doFinal(magicConstant); byte[] lmHash = new byte[16]; System.arraycopy(lowHash, 0, lmHash, 0, 8); System.arraycopy(highHash, 0, lmHash, 8, 8); return lmHash; */ return null; } /** * Creates the NTLM Hash of the user's password. * * @param password The password. * * @return The NTLM Hash of the given password, used in the calculation * of the NTLM Response and the NTLMv2 and LMv2 Hashes. */ private static byte[] ntlmHash(String password) throws UnsupportedEncodingException { // FIXME /* byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked"); MD4 md4 = new MD4(); md4.update(unicodePassword, 0, unicodePassword.length); byte[] ret = new byte[md4.getDigestSize()]; return ret; */ return null; } /** * Creates the NTLMv2 Hash of the user's password. * * @param target The authentication target (i.e., domain). * @param user The username. * @param password The password. * * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 * and LMv2 Responses. */ private static byte[] ntlmv2Hash(String target, String user, String password) throws UnsupportedEncodingException { byte[] ntlmHash = ntlmHash(password); String identity = user.toUpperCase() + target.toUpperCase(); return hmacMD5(identity.getBytes("UnicodeLittleUnmarked"), ntlmHash); } /** * Creates the LM Response from the given hash and Type 2 challenge. * * @param hash The LM or NTLM Hash. * @param challenge The server challenge from the Type 2 message. * * @return The response (either LM or NTLM, depending on the provided * hash). */ private static byte[] lmResponse(byte[] hash, byte[] challenge) { /* byte[] keyBytes = new byte[21]; System.arraycopy(hash, 0, keyBytes, 0, 16); Key lowKey = createDESKey(keyBytes, 0); Key middleKey = createDESKey(keyBytes, 7); Key highKey = createDESKey(keyBytes, 14); Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); des.init(Cipher.ENCRYPT_MODE, lowKey); byte[] lowResponse = des.doFinal(challenge); des.init(Cipher.ENCRYPT_MODE, middleKey); byte[] middleResponse = des.doFinal(challenge); des.init(Cipher.ENCRYPT_MODE, highKey); byte[] highResponse = des.doFinal(challenge); byte[] lmResponse = new byte[24]; System.arraycopy(lowResponse, 0, lmResponse, 0, 8); System.arraycopy(middleResponse, 0, lmResponse, 8, 8); System.arraycopy(highResponse, 0, lmResponse, 16, 8); return lmResponse; */ return null; } /** * Creates the LMv2 Response from the given hash, client data, and * Type 2 challenge. * * @param hash The NTLMv2 Hash. * @param clientData The client data (blob or client challenge). * @param challenge The server challenge from the Type 2 message. * * @return The response (either NTLMv2 or LMv2, depending on the * client data). */ private static byte[] lmv2Response(byte[] hash, byte[] clientData, byte[] challenge) { byte[] data = new byte[challenge.length + clientData.length]; System.arraycopy(challenge, 0, data, 0, challenge.length); System.arraycopy(clientData, 0, data, challenge.length, clientData.length); byte[] mac = hmacMD5(data, hash); byte[] lmv2Response = new byte[mac.length + clientData.length]; System.arraycopy(mac, 0, lmv2Response, 0, mac.length); System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length); return lmv2Response; } /** * Creates the NTLMv2 blob from the given target information block and * client challenge. * * @param targetInformation The target information block from the Type 2 * message. * @param clientChallenge The random 8-byte client challenge. * * @return The blob, used in the calculation of the NTLMv2 Response. */ private static byte[] createBlob(byte[] targetInformation, byte[] clientChallenge) { byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 }; byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; long time = System.currentTimeMillis(); time += 11644473600000l; // milliseconds from January 1, 1601 -> 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; } } } } } }