package javaforce.service;
/**
* A simple Proxy Server
*
* Supports : SSL (CONNECT)
*
* jfproxy.cfg example:
* [global]
* port=3128
* allow=0.0.0.0/0
* [blockdomain]
* .*youtube[.]com
* [urlchange]
* url = newURL
*
* Note : in [blockdomain] section the domains are in Regular Expression format
* see : http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html
*
* Note : in [urlchange] section the url is a regular expression
* and the = MUST have a space before and after it.
*
* @author pquiring
*
*/
import java.io.*;
import java.net.*;
import java.util.*;
import javaforce.*;
import javaforce.jbus.*;
public class Proxy extends Thread {
public final static String busPack = "net.sf.jfproxy";
public static String getConfigFile() {
return JF.getConfigPath() + "/jfproxy.cfg";
}
public static String getLogFile() {
return JF.getLogPath() + "/jfproxy.log";
}
private static class URLChange {
public String url, newurl;
}
private ServerSocket ss;
private Vector<Session> list = new Vector<Session>();
private ArrayList<String> blockedDomain = new ArrayList<String>();
private ArrayList<URLChange> urlChanges = new ArrayList<URLChange>();
private ArrayList<Integer> allow_net = new ArrayList<Integer>();
private ArrayList<Integer> allow_mask = new ArrayList<Integer>();
private int port = 3128;
public void close() {
JFLog.logTrace("proxy.close()");
try {
ss.close();
} catch (Exception e) {}
busClient.close();
//close list
Session sess;
while (list.size() > 0) {
sess = list.get(0);
sess.close();
}
}
public void run() {
JFLog.init(getLogFile(), true);
Socket s;
Session sess;
loadConfig();
busClient = new JBusClient(busPack, new JBusMethods());
busClient.setPort(getBusPort());
busClient.start();
//try to bind to port 5 times (in case restart() takes a while)
for(int a=0;a<5;a++) {
try {
ss = new ServerSocket(port);
} catch (Exception e) {
if (a == 4) return;
JF.sleep(1000);
continue;
}
break;
}
try {
JFLog.log("Starting proxy on port " + port);
//read newJS
while (!ss.isClosed()) {
s = ss.accept();
sess = new Session(s);
sess.start();
}
} catch (Exception e) {
JFLog.log(e);
}
}
private static enum Section {None, Global, BlockDomain, URLChange};
private final static String defaultConfig
= "[global]\n"
+ "port=3128\n"
+ "allow=0.0.0.0/0 #allow all\n"
+ "#allow=192.168.0.0/24 #allow subnet\n"
+ "#allow=10.1.2.3/32 #allow single ip\n"
+ "\n"
+ "[blockdomain]\n"
+ ".*youtube[.]com\n"
+ "\n"
+ "[urlchange]\n"
+ "#www.example.com/test = www.google.com\n";
private void loadConfig() {
Section section = Section.None;
try {
StringBuilder cfg = new StringBuilder();
BufferedReader br = new BufferedReader(new FileReader(getConfigFile()));
while (true) {
String ln = br.readLine();
if (ln == null) break;
cfg.append(ln);
cfg.append("\n");
ln = ln.trim().toLowerCase();
int idx = ln.indexOf('#');
if (idx != -1) ln = ln.substring(0, idx).trim();
if (ln.length() == 0) continue;
if (ln.equals("[global]")) {
section = Section.Global;
continue;
}
if (ln.equals("[blockdomain]")) {
section = Section.BlockDomain;
continue;
}
if (ln.equals("[urlchange]")) {
section = Section.URLChange;
continue;
}
switch (section) {
case Global:
if (ln.startsWith("port=")) {
port = JF.atoi(ln.substring(5));
}
if (ln.startsWith("allow=")) {
String net_mask = ln.substring(6);
idx = net_mask.indexOf("/");
String net = net_mask.substring(0, idx);
int addr = getIP(net);
allow_net.add(addr);
String mask = net_mask.substring(idx+1);
int maskBits = getMask(mask);
allow_mask.add(maskBits);
}
break;
case BlockDomain:
blockedDomain.add(ln);
break;
case URLChange:
int eq = ln.indexOf(" = ");
if (eq == -1) {
JFLog.log("Bad URLChange:" + ln);
break;
}
URLChange uc = new URLChange();
uc.url = ln.substring(0, eq);
uc.newurl = ln.substring(eq+3);
urlChanges.add(uc);
break;
}
}
config = cfg.toString();
} catch (FileNotFoundException e) {
//create default config
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(defaultConfig.getBytes());
fos.close();
config = defaultConfig;
} catch (Exception e2) {
JFLog.log(e2);
}
} catch (Exception e) {
JFLog.log(e);
}
}
private int ba2int(byte ba[]) {
int ret = 0;
for(int a=0;a<4;a++) {
ret <<= 8;
ret += ((int)ba[a]) & 0xff;
}
return ret;
}
private int getIP(String ip) {
String p[] = ip.split("[.]");
byte o[] = new byte[4];
for(int a=0;a<4;a++) {
o[a] = (byte)JF.atoi(p[a]);
}
return ba2int(o);
}
private int getMask(String mask) {
int bits = JF.atoi(mask);
if (bits == 0) return 0;
int ret = 0x80000000;
bits--;
while (bits > 0) {
ret >>= 1; //signed shift will repeat the sign bit (>>>=1 would not)
bits--;
}
return ret;
}
private class Session extends Thread {
private Socket p, i; //proxy, internet
private InputStream pis, iis;
private OutputStream pos, ios;
private boolean disconn = false;
private int client_port;
private String client_ip;
public synchronized void close() {
try {
if ((p!=null) && (p.isConnected())) p.close();
p = null;
} catch (Exception e1) {}
try {
if ((i!=null) && (i.isConnected())) i.close();
i = null;
} catch (Exception e2) {}
list.remove(this);
}
public Session(Socket s) {
p = s;
}
public String toString(int ip) {
long ip64 = ((long)ip) & 0xffffffffL;
return Long.toString(ip64, 16);
}
private void log(String s) {
JFLog.log(client_ip + ":" + client_port + ":" + s);
}
private void log(Exception e) {
String s = e.toString();
StackTraceElement stack[] = e.getStackTrace();
for(int a=0;a<stack.length;a++) {
s += "\r\n" + stack[a].toString();
}
log(s);
}
public void run() {
String req = "";
int ch;
list.add(this);
client_port = p.getPort();
client_ip = p.getInetAddress().getHostAddress();
log("Session Start");
try {
boolean allowed = false;
for(int a=0;a<allow_net.size();a++) {
int net = allow_net.get(a);
int mask = allow_mask.get(a);
int host = getIP(p);
if ((net & mask) == (host & mask)) {
allowed = true;
break;
}
}
if (!allowed) throw new Exception("client not allowed");
pis = p.getInputStream();
pos = p.getOutputStream();
while (true) {
req = "";
log("reading request");
do {
ch = pis.read();
if (ch == -1) throw new Exception("read error");
req += (char)ch;
} while (!req.endsWith("\r\n\r\n"));
proxy(req);
if (disconn) {
log("disconn");
break;
}
}
p.close();
} catch (Exception e) {
if (req.length() > 0) log(e);
}
close();
log("Session Stop");
}
private int getIP(Socket s) {
if (s.getInetAddress().isLoopbackAddress()) return 0x7f000001; //loopback may return IP6 address
byte o[] = s.getInetAddress().getAddress();
return ba2int(o);
}
private void proxy(String req) throws Exception {
String ln[] = req.split("\r\n");
log("Proxy:" + ln[0]);
int hostidx = -1;
if (ln[0].endsWith("1.0")) disconn = true; //HTTP/1.0
for(int a=0;a<ln.length;a++) {
if (ln[a].regionMatches(true, 0, "Host: ", 0, 6)) hostidx = a;
}
if (hostidx == -1) {
log("ERROR : No host specified : " + req);
replyError(505, "No host specified");
return;
}
String hostln = ln[hostidx].substring(6); //"Host: "
String host;
try {
String method = null, proto = null, url = null, http = null;
int port;
String f[] = ln[0].split(" ");
method = f[0];
url = f[1];
http = f[2];
if (url.startsWith("http://")) {
proto = "http://";
url = url.substring(7);
port = 80;
} else if (url.startsWith("ftp://")) {
proto = "ftp://";
url = url.substring(6);
port = 21;
} else {
proto = "http://"; //assume http
port = 80;
}
int portidx = hostln.indexOf(':');
if (portidx != -1) {
host = hostln.substring(0, portidx);
port = Integer.valueOf(hostln.substring(portidx+1));
} else {
host = hostln;
}
//check if host is blocked
host = host.trim().toLowerCase();
for(int a=0;a<blockedDomain.size();a++) {
if (host.matches(blockedDomain.get(a))) {
replyError(505, "Access Denied");
return;
}
}
if (method.equals("CONNECT")) {
connectCommand(host, ln[0]);
return;
}
//check if url is changed
for(int a=0;a<urlChanges.size();a++) {
URLChange uc = urlChanges.get(a);
if (url.matches(uc.url)) {
url = uc.newurl;
ln[0] = method + " " + proto + url + " " + http;
int iport = url.indexOf(":");
int iurl = url.indexOf("/");
if (iurl == -1) iurl = url.length();
if (iport == -1) {
port = 80;
host = url.substring(0, iurl);
ln[hostidx] = "Host: " + host;
} else {
port = JF.atoi(url.substring(iport+1, iurl));
host = url.substring(0, iport);
ln[hostidx] = "Host: " + host + ":" + port;
}
break;
}
}
if (proto.equals("http://")) {
connect(host, port);
sendRequest(ln);
if (method.equals("POST")) sendPost(ln);
relayReply(proto + url);
} else {
ftp(host, port, url);
}
return;
} catch (UnknownHostException uhe) {
replyError(404, "Domain not found");
log(uhe);
} catch (IOException ioe) {
/*do nothing*/
log(ioe);
} catch (Exception e) {
replyError(505, "Exception:" + e);
log(e);
}
}
private void connect(String host, int port) throws UnknownHostException, IOException {
log("connect:" + host + ":" + port);
i = new Socket(host, port);
iis = i.getInputStream();
ios = i.getOutputStream();
}
private void ftp(String host, int port, String url) throws Exception {
int idx = url.indexOf('/');
url = url.substring(idx); //remove host
FTP ftp = new FTP();
log("ftp:" + url);
try {
ftp.connect(host, port);
ftp.login("anonymous", "nobody@jfproxy.sf.net");
if (url.endsWith("/")) {
//directory listing
String ls = ftp.ls(url);
String lns[] = ls.replaceAll("\r", "").split("\n");
StringBuffer content = new StringBuffer();
content.append("<html><head>");
//TODO : fix this for Firefox - only works with Chrome
content.append("<style type=\"text/css\">\r\n");
content.append(".file\r\n {\r\nbackground :\r\n url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABHUlEQVR42o2RMW7DIBiF3498iHRJD5JKHurL+CRVBp+i2T16tTynF2gO0KSb5ZrBBl4HHDBuK/WXACH4eO9/CAAAbdvijzLGNE1TVZXfZuHg6XCAQESAZXbOKaXO57eiKG6ft9PrKQIkCQqFoIiQFBGlFIB5nvM8t9aOX2Nd18oDzjnPgCDpn/BH4zh2XZdlWVmWiUK4IgCBoFMUz9eP6zRN75cLgEQhcmTQIbl72O0f9865qLAAsURAAgKBJKEtgLXWvyjLuFsThCSstb8rBCaAQhDYWgIZ7myM+TUBjDHrHlZcbMYYk34cN0YSLcgS+wL0fe9TXDMbY33fR2AYBvyQ8L0Gk8MwREBrTfKe4TpTzwhArXWi8HI84h/1DfwI5mhxJamFAAAAAElFTkSuQmCC') left top no-repeat;\r\npadding-left : 24px;\r\n}\r\n");
content.append(".folder\r\n {\r\nbackground :\r\n url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAd5JREFUeNqMU79rFUEQ/vbuodFEEkzAImBpkUabFP4ldpaJhZXYm/RiZWsv/hkWFglBUyTIgyAIIfgIRjHv3r39MePM7N3LcbxAFvZ2b2bn22/mm3XMjF+HL3YW7q28YSIw8mBKoBihhhgCsoORot9d3/ywg3YowMXwNde/PzGnk2vn6PitrT+/PGeNaecg4+qNY3D43vy16A5wDDd4Aqg/ngmrjl/GoN0U5V1QquHQG3q+TPDVhVwyBffcmQGJmSVfyZk7R3SngI4JKfwDJ2+05zIg8gbiereTZRHhJ5KCMOwDFLjhoBTn2g0ghagfKeIYJDPFyibJVBtTREwq60SpYvh5++PpwatHsxSm9QRLSQpEVSd7/TYJUb49TX7gztpjjEffnoVw66+Ytovs14Yp7HaKmUXeX9rKUoMoLNW3srqI5fWn8JejrVkK0QcrkFLOgS39yoKUQe292WJ1guUHG8K2o8K00oO1BTvXoW4yasclUTgZYJY9aFNfAThX5CZRmczAV52oAPoupHhWRIUUAOoyUIlYVaAa/VbLbyiZUiyFbjQFNwiZQSGl4IDy9sO5Wrty0QLKhdZPxmgGcDo8ejn+c/6eiK9poz15Kw7Dr/vN/z6W7q++091/AQYA5mZ8GYJ9K0AAAAAASUVORK5CYII=') left top no-repeat;\r\npadding-left : 24px;\r\n}\r\n");
content.append(".up\r\n {\r\nbackground :\r\n url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU0toU0EUPfPysx/tTxuDH9SCWhUDooIbd7oRUUTMouqi2iIoCO6lceHWhegy4EJFinWjrlQUpVm0IIoFpVDEIthm0dpikpf3ZuZ6Z94nrXhhMjM3c8895977BBHB2PznK8WPtDgyWH5q77cPH8PpdXuhpQT4ifR9u5sfJb1bmw6VivahATDrxcRZ2njfoaMv+2j7mLDn93MPiNRMvGbL18L9IpF8h9/TN+EYkMffSiOXJ5+hkD+PdqcLpICWHOHc2CC+LEyA/K+cKQMnlQHJX8wqYG3MAJy88Wa4OLDvEqAEOpJd0LxHIMdHBziowSwVlF8D6QaicK01krw/JynwcKoEwZczewroTvZirlKJs5CqQ5CG8pb57FnJUA0LYCXMX5fibd+p8LWDDemcPZbzQyjvH+Ki1TlIciElA7ghwLKV4kRZstt2sANWRjYTAGzuP2hXZFpJ/GsxgGJ0ox1aoFWsDXyyxqCs26+ydmagFN/rRjymJ1898bzGzmQE0HCZpmk5A0RFIv8Pn0WYPsiu6t/Rsj6PauVTwffTSzGAGZhUG2F06hEc9ibS7OPMNp6ErYFlKavo7MkhmTqCxZ/jwzGA9Hx82H2BZSw1NTN9Gx8ycHkajU/7M+jInsDC7DiaEmo1bNl1AMr9ASFgqVu9MCTIzoGUimXVAnnaN0PdBBDCCYbEtMk6wkpQwIG0sn0PQIUF4GsTwLSIFKNqF6DVrQq+IWVrQDxAYQC/1SsYOI4pOxKZrfifiUSbDUisif7XlpGIPufXd/uvdvZm760M0no1FZcnrzUdjw7au3vu/BVgAFLXeuTxhTXVAAAAAElFTkSuQmCC') left top no-repeat;\r\npadding-left : 24px;\r\n}\r\n");
content.append("</style>\r\n");
content.append("</head><body>");
if (!url.equals("/")) {
//add up link
int upidx = url.substring(0, url.length() - 1).lastIndexOf("/");
String up = url.substring(0, upidx+1);
content.append("<a class='up' href='" + up + "'>[parent folder]</a><br>\r\n");
}
for(int a=0;a<lns.length;a++) {
String f[] = lns[a].split(" ", -1); //drwxrwxrwx ? uid gid size month day time_or_year filename
int last = f.length - 1;
if (f[0].charAt(0) == 'd') {
//folder
content.append("<a class='folder' href='" + f[last] + "/'>[" + f[last] + "]</a><br>\r\n");
} else {
//file
content.append("<a class='file' href='" + f[last] + "'>" + f[last] + "</a><br>\r\n");
}
}
content.append("</body></html>");
int code = 200;
String msg = "OK";
String headers = "HTTP/1.1 " + code + " " + msg + "\r\nContent-Type: text/html\r\nContent-Length: " + content.length() + "\r\n\r\n";
pos.write(headers.getBytes());
pos.write(content.toString().getBytes());
pos.flush();
} else {
//download file
int code = 200;
String msg = "OK";
String headers = "HTTP/1.0 " + code + " " + msg + "\r\n\r\n";
disconn = true;
pos.write(headers.getBytes());
ftp.get(url, pos);
pos.flush();
}
} catch (Exception e) {
replyError(505, "Exception:" + e);
log(e);
}
}
private void replyError(int code, String msg) throws Exception {
log("Error:" + code);
String content = "<h1>Error : " + code + " : " + msg + "</h1>";
String headers = "HTTP/1.1 " + code + " " + msg + "\r\nContent-Length: " + content.length() + "\r\n\r\n";
pos.write(headers.getBytes());
pos.write(content.getBytes());
pos.flush();
}
private void sendRequest(String ln[]) throws Exception {
String req = "";
for(int a=0;a<ln.length;a++) {
if (a == 0) ln[a] = removeHost(ln[a]);
req += ln[a];
req += "\r\n";
}
req += "\r\n";
ios.write(req.getBytes());
ios.flush();
}
private void sendPost(String ln[]) throws Exception {
int length = -1;
for(int a=0;a<ln.length;a++) {
if (ln[a].regionMatches(true, 0, "Content-Length: ", 0, 16)) {
length = Integer.valueOf(ln[a].substring(16, ln[a].length()));
}
}
if (length == -1) throw new Exception("unknown post size");
log("sendPost data len=" + length);
byte post[] = JF.readAll(pis, length);
ios.write(post);
ios.flush();
}
private void relayReply(String fn) throws Exception {
log("relayReply:" + fn);
String tmp[];
String line = "";
String headers = "";
int length = -1;
int contentLength = -1;
int ch;
boolean first = true;
int code;
String encoding = "";
do {
ch = iis.read();
if (ch == -1) throw new Exception("read error");
line += (char)ch;
if (!line.endsWith("\r\n")) continue;
if (line.regionMatches(true, 0, "Content-Length: ", 0, 16)) {
length = Integer.valueOf(line.substring(16, line.length() - 2));
contentLength = length;
}
if (line.regionMatches(true, 0, "Connection: Close", 0, 17)) {
disconn = true;
}
if (line.regionMatches(true, 0, "Transfer-Encoding:", 0, 18)) {
encoding = line.substring(18).trim().toLowerCase();
}
if (first == true) {
//HTTP/1.1 CODE MSG
if (line.startsWith("HTTP/1.0")) disconn = true;
tmp = line.split(" ");
code = Integer.valueOf(tmp[1]);
log("reply=" + code + ":" + line);
first = false;
}
headers += line;
if (line.length() == 2) break; //blank line (double enter)
line = "";
} while (true);
pos.write(headers.getBytes());
pos.flush();
if (length == 0) {
log("reply:done:content.length=0:headers.length=" + headers.length());
return;
}
if (length == -1) {
if (encoding.equals("chunked")) {
//read chunked format
contentLength = 0;
while (true) {
//read chunk size followed by \r\n
String chunkSize = "";
while (true) {
ch = iis.read();
if (ch == -1) throw new Exception("read error");
chunkSize += (char)ch;
if (chunkSize.endsWith("\r\n")) break;
}
contentLength += chunkSize.length();
int idx = chunkSize.indexOf(";"); //ignore extensions
if (idx == -1) idx = chunkSize.length() - 2;
int chunkLength = Integer.valueOf(chunkSize.substring(0, idx), 16);
pos.write(chunkSize.getBytes());
boolean zero = chunkLength == 0;
//read chunk
chunkLength += 2; // \r\n
contentLength += chunkLength;
int read , off = 0;
byte buf[] = new byte[chunkLength];
while (chunkLength != 0) {
read = iis.read(buf, off, chunkLength);
if (read == -1) throw new Exception("read error");
if (read > 0) {
chunkLength -= read;
off += read;
}
}
pos.write(buf);
pos.flush();
if (zero) break;
}
} else {
contentLength = 0;
//read until disconnected (HTTP/1.0)
int read;
byte buf[] = new byte[64 * 1024];
while (true) {
read = iis.read(buf, 0, 64 * 1024);
if (read == -1) break;
if (read > 0) {
contentLength += read;
pos.write(buf, 0, read);
pos.flush();
}
}
}
} else {
//read content (length known)
int read, off = 0;
byte buf[] = new byte[length];
while (length != 0) {
read = iis.read(buf, off, length);
if (read == -1) break;
if (read > 0) {
length -= read;
off += read;
}
}
pos.write(buf);
pos.flush();
}
log("reply:done:content.length=" + contentLength + ":headers.length=" + headers.length());
}
private void connectCommand(String host, String req) throws Exception {
String ln[] = req.split(" ");
if (ln.length != 3) {
replyError(505, "Bad CONNECT syntax");
return;
}
int portidx = ln[1].indexOf(':');
if (portidx != -1) {
int port = Integer.valueOf(ln[1].substring(portidx+1));
if (port != 443) {
replyError(505, "CONNECT is for port 443 only");
return;
}
}
connect(host, 443);
pos.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
pos.flush();
ConnectRelay i2p = new ConnectRelay(iis, pos);
ConnectRelay p2i = new ConnectRelay(pis, ios);
i2p.start();
p2i.start();
i2p.join();
p2i.join();
disconn = true; //not HTTP/1.1 compatible?
}
private String removeHost(String req) throws Exception {
//older webserver don't like the host in the request line
//in fact I didn't even know that some would accept it
String p[] = req.split(" ");
if (p.length != 3) return req;
URL url = new URL(p[1]);
return p[0] + " " + url.getFile() + " " + p[2];
}
private class ConnectRelay extends Thread {
private InputStream is;
private OutputStream os;
private byte buf[] = new byte[4096];
private final int buflen = 4096;
public ConnectRelay(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
public void run() {
int read;
try {
while (true) {
read = is.read(buf, 0, buflen);
if (read == -1) break;
if (read > 0) {os.write(buf, 0, read); os.flush();}
}
} catch (Exception e) {}
}
}
}
private static JBusServer busServer;
private JBusClient busClient;
private String config;
public class JBusMethods {
public void getConfig(String pack) {
busClient.call(pack, "getConfig", busClient.quote(busClient.encodeString(config)));
}
public void setConfig(String cfg) {
//write new file
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(JBusClient.decodeString(cfg).getBytes());
fos.close();
} catch (Exception e) {
JFLog.log(e);
}
}
public void restart() {
proxy.close();
proxy = new Proxy();
proxy.start();
}
}
public static int getBusPort() {
if (JF.isWindows()) {
return 33003;
} else {
return 777;
}
}
public static void main(String args[]) {
serviceStart(args);
}
//Win32 Service
private static Proxy proxy;
public static void serviceStart(String args[]) {
if (JF.isWindows()) {
busServer = new JBusServer(getBusPort());
busServer.start();
while (!busServer.ready) {
JF.sleep(10);
}
}
proxy = new Proxy();
proxy.start();
}
public static void serviceStop() {
JFLog.log("Stopping service");
busServer.close();
proxy.close();
}
}