diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..206bda08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +classes +bin +gen +obj +target +local.properties +.classpath +.project +.settings +tests/bin +tests/gen +tests/local.properties +NUL +libs + +#Intellij IDEA +*.iml +*.ipr +*.iws +.idea/ +out/ diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100755 index 00000000..1f75f8a2 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/iptables b/assets/iptables new file mode 100644 index 00000000..7268216c Binary files /dev/null and b/assets/iptables differ diff --git a/assets/pdnsd b/assets/pdnsd new file mode 100755 index 00000000..43b80fd8 Binary files /dev/null and b/assets/pdnsd differ diff --git a/assets/pdnsd.conf b/assets/pdnsd.conf new file mode 100644 index 00000000..93ca5de8 --- /dev/null +++ b/assets/pdnsd.conf @@ -0,0 +1,43 @@ +global { + perm_cache = 2048; + cache_dir = "/data/data/com.github.shadowsocks"; + server_ip = 127.0.0.1; + server_port = 8053; + status_ctl = on; + paranoid = on; + query_method = tcp_only; + min_ttl = 15m; + max_ttl = 1w; + timeout = 10; + daemon = on; + pid_file = "/data/data/com.github.shadowsocks/pdnsd.pid"; +} + +server { + label = "root-servers"; + root_server = on; + + ip = 8.8.8.8 + , 8.8.4.4 + , 208.67.222.222 + , 208.67.220.220 + ; + + timeout = 5; + + uptest = query; + interval = 30m; // Test every half hour. + ping_timeout = 300; // 30 seconds. + purge_cache = off; + exclude = .localdomain; + policy = included; + preset = off; +} + +rr { + name=localhost; + reverse=on; + a=127.0.0.1; + owner=localhost; + soa=localhost,root.localhost,42,86400,900,86400,86400; +} diff --git a/assets/redsocks b/assets/redsocks new file mode 100755 index 00000000..0454bf77 Binary files /dev/null and b/assets/redsocks differ diff --git a/assets/shadowsocks b/assets/shadowsocks new file mode 100755 index 00000000..453daa2c Binary files /dev/null and b/assets/shadowsocks differ diff --git a/jni/Android.mk b/jni/Android.mk index 91464352..4ff57c15 100755 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -24,18 +24,15 @@ LOCAL_SRC_FILES := \ include $(BUILD_STATIC_LIBRARY) -#include $(CLEAR_VARS) +include $(CLEAR_VARS) -#PDNSD_SOURCES := $(wildcard $(LOCAL_PATH)/pdnsd/*.c) +PDNSD_SOURCES := $(wildcard $(LOCAL_PATH)/pdnsd/*.c) -#LOCAL_MODULE := pdnsd -#LOCAL_SRC_FILES := $(PDNSD_SOURCES:$(LOCAL_PATH)%=%) +LOCAL_MODULE := pdnsd +LOCAL_SRC_FILES := $(PDNSD_SOURCES:$(LOCAL_PATH)%=%) +LOCAL_CFLAGS := -Wall -O2 -I$(LOCAL_PATH)/pdnsd -#LOCAL_CFLAGS := -O3 - -#LOCAL_STATIC_LIBRARIES := - -#include $(BUILD_STATIC_LIBRARY) +include $(BUILD_EXECUTABLE) include $(CLEAR_VARS) @@ -47,6 +44,17 @@ LOCAL_STATIC_LIBRARIES := libev libcrypto LOCAL_LDLIBS := -llog +include $(BUILD_EXECUTABLE) + +include $(CLEAR_VARS) + +LOCAL_MODULE:= libexec + +LOCAL_SRC_FILES:= \ + termExec.cpp + +LOCAL_LDLIBS := -ldl -llog + include $(BUILD_SHARED_LIBRARY) subdirs := $(addprefix $(LOCAL_PATH)/openssl/,$(addsuffix /Android.mk, \ diff --git a/jni/pdnsd/conff.c b/jni/pdnsd/conff.c index 83b2f686..32f4e885 100644 --- a/jni/pdnsd/conff.c +++ b/jni/pdnsd/conff.c @@ -166,35 +166,6 @@ int read_config_file(const char *nm, globparm_t *global, servparm_array *servers *errstr=NULL; goto close_file; } - else if (sb.st_uid!=init_uid) { - /* Note by Paul Rombouts: - Perhaps we should use getpwuid_r() instead of getpwuid(), which is not necessarily thread safe. - As long as getpwuid() is only used by only one thread, it should be OK, - but it is something to keep in mind. - */ - struct passwd *pws; - char owner[24],user[24]; - if((pws=getpwuid(sb.st_uid))) - strncp(owner,pws->pw_name,sizeof(owner)); - else - sprintf(owner,"%i",sb.st_uid); - if((pws=getpwuid(init_uid))) - strncp(user,pws->pw_name,sizeof(user)); - else - sprintf(user,"%i",init_uid); - if(asprintf(errstr, - "Error: %s file %s is owned by '%s', but pdnsd was started as user '%s'.", - conftype,nm,owner,user)<0) - *errstr=NULL; - goto close_file; - } - else if ((sb.st_mode&(S_IWGRP|S_IWOTH))) { - if(asprintf(errstr, - "Error: Bad %s file permissions: file %s must be only writeable by the user.", - conftype,nm)<0) - *errstr=NULL; - goto close_file; - } } retval=confparse(in,NULL,global,servers,includedepth,errstr); diff --git a/jni/pdnsd/config.h b/jni/pdnsd/config.h index 7d0a69ab..601afae7 100644 --- a/jni/pdnsd/config.h +++ b/jni/pdnsd/config.h @@ -40,7 +40,7 @@ * In normal operation, you will currently only need IPv4. */ #define ENABLE_IPV4 1 #define DEFAULT_IPV4 1 -/* #undef ENABLE_IPV6 */ +#undef ENABLE_IPV6 /* In all pdnsd versions before 1.0.6, DNS queries were always done over * TCP. Now, you have the choice. You can control that behaviour using @@ -55,7 +55,7 @@ * error or times out, the query is retried using UDP. * UDP_TCP: UDP, then TCP. If the UDP reply is truncated (i.e. the tc flag is set), * the query is retried using TCP. */ -#define M_PRESET UDP_ONLY +#define M_PRESET TCP_ONLY /* In addition to choosing the presets, you may also completely disable * one of the protocols (TCP for preset UDP_ONLY and UDP for preset TCP_ONLY). @@ -104,7 +104,7 @@ * default: native; others: gdbm */ #define CACHE_DBM DBM_NATIVE -#define CACHEDIR "/var/cache/pdnsd" +#define CACHEDIR "/data/data/com.github.shadowsocks" #define TEMPDIR "/tmp"; @@ -214,10 +214,10 @@ #define HAVE_FCNTL_H 1 /* Define to 1 if you have the `getline' function. */ -#define HAVE_GETLINE 1 +//#define HAVE_GETLINE 1 /* Define to 1 if you have the `getpwnam_r' function. */ -#define HAVE_GETPWNAM_R 1 +//#define HAVE_GETPWNAM_R 1 /* Define to 1 if you have the `gettimeofday' function. */ #define HAVE_GETTIMEOFDAY 1 @@ -241,7 +241,7 @@ #define HAVE_MEMORY_H 1 /* Define to 1 if you have the `mempcpy' function. */ -#define HAVE_MEMPCPY 1 +//#define HAVE_MEMPCPY 1 /* Define to 1 if you have the `mkfifo' function. */ #define HAVE_MKFIFO 1 @@ -277,10 +277,10 @@ #define HAVE_STDLIB_H 1 /* Define to 1 if you have the `stpcpy' function. */ -#define HAVE_STPCPY 1 +//#define HAVE_STPCPY 1 /* Define to 1 if you have the `stpncpy' function. */ -#define HAVE_STPNCPY 1 +//#define HAVE_STPNCPY 1 /* Define to 1 if you have the `strdup' function. */ #define HAVE_STRDUP 1 diff --git a/jni/pdnsd/main.c b/jni/pdnsd/main.c index 1190b77c..c9568765 100644 --- a/jni/pdnsd/main.c +++ b/jni/pdnsd/main.c @@ -67,7 +67,7 @@ FILE *dbg_file=NULL; volatile int tcp_socket=-1; volatile int udp_socket=-1; sigset_t sigs_msk; -char *conf_file=CONFDIR"/pdnsd.conf"; +char *conf_file="pdnsd.conf"; /* version and licensing information */ @@ -140,7 +140,7 @@ static const char help_message[] = "\n" "-c\t\t--or--\n" "--config-file\tspecifies the file the configuration is read from.\n" - "\t\tDefault is " CONFDIR "/pdnsd.conf\n" + "\t\tDefault is " "pdnsd.conf\n" #ifdef ENABLE_IPV4 "-4\t\tswitches to IPv4 mode.\n" "\t\t" diff --git a/jni/shadowsocks/local.c b/jni/shadowsocks/local.c index 52dfc14b..cc89c84f 100644 --- a/jni/shadowsocks/local.c +++ b/jni/shadowsocks/local.c @@ -18,7 +18,6 @@ #include #include #include -#include #include "local.h" #include "encrypt.h" @@ -38,440 +37,469 @@ static char *_server; static char *_remote_port; struct client_ctx { - ev_io io; - int fd; + ev_io io; + int fd; }; int setnonblocking(int fd) { - int flags; - if (-1 ==(flags = fcntl(fd, F_GETFL, 0))) - flags = 0; - return fcntl(fd, F_SETFL, flags | O_NONBLOCK); + int flags; + if (-1 ==(flags = fcntl(fd, F_GETFL, 0))) + flags = 0; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int create_and_bind(const char *port) { - struct addrinfo hints; - struct addrinfo *result, *rp; - int s, listen_sock; + struct addrinfo hints; + struct addrinfo *result, *rp; + int s, listen_sock; - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */ - hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */ - hints.ai_flags = AI_PASSIVE; /* All interfaces */ + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */ + hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */ + hints.ai_flags = AI_PASSIVE; /* All interfaces */ - s = getaddrinfo("0.0.0.0", port, &hints, &result); - if (s != 0) { - fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s)); - return -1; - } + s = getaddrinfo("0.0.0.0", port, &hints, &result); + if (s != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s)); + return -1; + } - for (rp = result; rp != NULL; rp = rp->ai_next) { - listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - if (listen_sock == -1) - continue; + for (rp = result; rp != NULL; rp = rp->ai_next) { + listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + int opt = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + if (listen_sock == -1) + continue; - s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); - if (s == 0) { - /* We managed to bind successfully! */ - break; - } else { - perror("bind"); - } + s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); + if (s == 0) { + /* We managed to bind successfully! */ + break; + } else { + perror("bind"); + } - close(listen_sock); - } + close(listen_sock); + } - if (rp == NULL) { - fprintf(stderr, "Could not bind\n"); - return -1; - } + if (rp == NULL) { + fprintf(stderr, "Could not bind\n"); + return -1; + } - freeaddrinfo(result); + freeaddrinfo(result); - return listen_sock; + return listen_sock; } static void server_recv_cb (EV_P_ ev_io *w, int revents) { - struct server_ctx *server_recv_ctx = (struct server_ctx *)w; - struct server *server = server_recv_ctx->server; - struct remote *remote = server->remote; - if (remote == NULL) { - close_and_free_server(EV_A_ server); - return; - } - while (1) { - ssize_t r = recv(server->fd, remote->buf, BUF_SIZE, 0); - if (r == 0) { - // connection closed - remote->buf_len = 0; - close_and_free_server(EV_A_ server); - if (remote != NULL) { - ev_io_start(EV_A_ &remote->send_ctx->io); - } - return; - } else if(r < 0) { - perror("recv"); - if (errno == EAGAIN) { - // no data - // continue to wait for recv - break; - } else { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - } - encrypt(remote->buf, r); - int w = send(remote->fd, remote->buf, r, MSG_NOSIGNAL); - if(w == -1) { - perror("send"); - if (errno == EAGAIN) { - // no data, wait for send - ev_io_stop(EV_A_ &server_recv_ctx->io); - ev_io_start(EV_A_ &remote->send_ctx->io); - break; - } else { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - } else if(w < r) { - char *pt; - for (pt = remote->buf; pt < pt + min(w, BUF_SIZE); pt++) { - *pt = *(pt + w); - } - remote->buf_len = r - w; - ev_io_stop(EV_A_ &server_recv_ctx->io); - ev_io_start(EV_A_ &remote->send_ctx->io); - break; - } - } + struct server_ctx *server_recv_ctx = (struct server_ctx *)w; + struct server *server = server_recv_ctx->server; + struct remote *remote = server->remote; + if (remote == NULL) { + close_and_free_server(EV_A_ server); + return; + } + while (1) { + ssize_t r = recv(server->fd, remote->buf, BUF_SIZE, 0); + if (r == 0) { + // connection closed + remote->buf_len = 0; + close_and_free_server(EV_A_ server); + if (remote != NULL) { + ev_io_start(EV_A_ &remote->send_ctx->io); + } + return; + } else if(r < 0) { + perror("recv"); + if (errno == EAGAIN) { + // no data + // continue to wait for recv + break; + } else { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + } + encrypt(remote->buf, r); + int w = send(remote->fd, remote->buf, r, MSG_NOSIGNAL); + if(w == -1) { + perror("send"); + if (errno == EAGAIN) { + // no data, wait for send + ev_io_stop(EV_A_ &server_recv_ctx->io); + ev_io_start(EV_A_ &remote->send_ctx->io); + break; + } else { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + } else if(w < r) { + char *pt; + for (pt = remote->buf; pt < pt + min(w, BUF_SIZE); pt++) { + *pt = *(pt + w); + } + remote->buf_len = r - w; + ev_io_stop(EV_A_ &server_recv_ctx->io); + ev_io_start(EV_A_ &remote->send_ctx->io); + break; + } + } } static void server_send_cb (EV_P_ ev_io *w, int revents) { - struct server_ctx *server_send_ctx = (struct server_ctx *)w; - struct server *server = server_send_ctx->server; - struct remote *remote = server->remote; - if (server->buf_len == 0) { - // close and free - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } else { - // has data to send - ssize_t r = send(server->fd, server->buf, - server->buf_len, 0); - if (r < 0) { - perror("send"); - if (errno != EAGAIN) { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - return; - } - if (r < server->buf_len) { - // printf("r=%d\n", r); - // printf("server->buf_len=%d\n", server->buf_len); - // partly sent, move memory, wait for the next time to send - char *pt; - for (pt = server->buf; pt < pt + min(r, BUF_SIZE); pt++) { - *pt = *(pt + r); - } - server->buf_len -= r; - return; - } else { - // all sent out, wait for reading - ev_io_stop(EV_A_ &server_send_ctx->io); - if (remote != NULL) { - ev_io_start(EV_A_ &remote->recv_ctx->io); - } else { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - } - } + struct server_ctx *server_send_ctx = (struct server_ctx *)w; + struct server *server = server_send_ctx->server; + struct remote *remote = server->remote; + if (server->buf_len == 0) { + // close and free + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } else { + // has data to send + ssize_t r = send(server->fd, server->buf, + server->buf_len, 0); + if (r < 0) { + perror("send"); + if (errno != EAGAIN) { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + return; + } + if (r < server->buf_len) { + // printf("r=%d\n", r); + // printf("server->buf_len=%d\n", server->buf_len); + // partly sent, move memory, wait for the next time to send + char *pt; + for (pt = server->buf; pt < pt + min(r, BUF_SIZE); pt++) { + *pt = *(pt + r); + } + server->buf_len -= r; + return; + } else { + // all sent out, wait for reading + ev_io_stop(EV_A_ &server_send_ctx->io); + if (remote != NULL) { + ev_io_start(EV_A_ &remote->recv_ctx->io); + } else { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + } + } } static void remote_recv_cb (EV_P_ ev_io *w, int revents) { - struct remote_ctx *remote_recv_ctx = (struct remote_ctx *)w; - struct remote *remote = remote_recv_ctx->remote; - struct server *server = remote->server; - if (server == NULL) { - close_and_free_remote(EV_A_ remote); - return; - } - while (1) { - ssize_t r = recv(remote->fd, server->buf, BUF_SIZE, 0); - // printf("after recv: r=%d\n", r); - if (r == 0) { - // connection closed - server->buf_len = 0; - close_and_free_remote(EV_A_ remote); - if (server != NULL) { - ev_io_start(EV_A_ &server->send_ctx->io); - } - return; - } else if(r < 0) { - perror("recv"); - if (errno == EAGAIN) { - // no data - // continue to wait for recv - break; - } else { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - } - decrypt(server->buf, r); - int w = send(server->fd, server->buf, r, MSG_NOSIGNAL); - // printf("after send: w=%d\n", w); - if(w == -1) { - perror("send"); - if (errno == EAGAIN) { - // no data, wait for send - ev_io_stop(EV_A_ &remote_recv_ctx->io); - ev_io_start(EV_A_ &server->send_ctx->io); - break; - } else { - close_and_free_server(EV_A_ server); - close_and_free_remote(EV_A_ remote); - return; - } - } else if(w < r) { - char *pt; - for (pt = server->buf; pt < pt + min(w, BUF_SIZE); pt++) { - *pt = *(pt + w); - } - server->buf_len = r - w; - ev_io_stop(EV_A_ &remote_recv_ctx->io); - ev_io_start(EV_A_ &server->send_ctx->io); - break; - } - } + struct remote_ctx *remote_recv_ctx = (struct remote_ctx *)w; + struct remote *remote = remote_recv_ctx->remote; + struct server *server = remote->server; + if (server == NULL) { + close_and_free_remote(EV_A_ remote); + return; + } + while (1) { + ssize_t r = recv(remote->fd, server->buf, BUF_SIZE, 0); + // printf("after recv: r=%d\n", r); + if (r == 0) { + // connection closed + server->buf_len = 0; + close_and_free_remote(EV_A_ remote); + if (server != NULL) { + ev_io_start(EV_A_ &server->send_ctx->io); + } + return; + } else if(r < 0) { + perror("recv"); + if (errno == EAGAIN) { + // no data + // continue to wait for recv + break; + } else { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + } + decrypt(server->buf, r); + int w = send(server->fd, server->buf, r, MSG_NOSIGNAL); + // printf("after send: w=%d\n", w); + if(w == -1) { + perror("send"); + if (errno == EAGAIN) { + // no data, wait for send + ev_io_stop(EV_A_ &remote_recv_ctx->io); + ev_io_start(EV_A_ &server->send_ctx->io); + break; + } else { + close_and_free_server(EV_A_ server); + close_and_free_remote(EV_A_ remote); + return; + } + } else if(w < r) { + char *pt; + for (pt = server->buf; pt < pt + min(w, BUF_SIZE); pt++) { + *pt = *(pt + w); + } + server->buf_len = r - w; + ev_io_stop(EV_A_ &remote_recv_ctx->io); + ev_io_start(EV_A_ &server->send_ctx->io); + break; + } + } } static void remote_send_cb (EV_P_ ev_io *w, int revents) { - struct remote_ctx *remote_send_ctx = (struct remote_ctx *)w; - struct remote *remote = remote_send_ctx->remote; - struct server *server = remote->server; - if (!remote_send_ctx->connected) { - socklen_t len; - struct sockaddr_storage addr; - len = sizeof addr; - int r = getpeername(remote->fd, (struct sockaddr*)&addr, &len); - if (r == 0) { - remote_send_ctx->connected = 1; - ev_io_stop(EV_A_ &remote_send_ctx->io); - ev_io_start(EV_A_ &server->recv_ctx->io); - ev_io_start(EV_A_ &remote->recv_ctx->io); - } else { - perror("getpeername"); - // not connected - close_and_free_remote(EV_A_ remote); - close_and_free_server(EV_A_ server); - return; - } - } else { - if (remote->buf_len == 0) { - // close and free - close_and_free_remote(EV_A_ remote); - close_and_free_server(EV_A_ server); - return; - } else { - // has data to send - ssize_t r = send(remote->fd, remote->buf, - remote->buf_len, 0); - if (r < 0) { - perror("send"); - if (errno != EAGAIN) { - // close and free - close_and_free_remote(EV_A_ remote); - close_and_free_server(EV_A_ server); - return; - } - return; - } - if (r < remote->buf_len) { - // partly sent, move memory, wait for the next time to send - char *pt; - for (pt = remote->buf; pt < pt + min(r, BUF_SIZE); pt++) { - *pt = *(pt + r); - } - remote->buf_len -= r; - return; - } else { - // all sent out, wait for reading - ev_io_stop(EV_A_ &remote_send_ctx->io); - if (server != NULL) { - ev_io_start(EV_A_ &server->recv_ctx->io); - } else { - close_and_free_remote(EV_A_ remote); - close_and_free_server(EV_A_ server); - return; - } - } - } + struct remote_ctx *remote_send_ctx = (struct remote_ctx *)w; + struct remote *remote = remote_send_ctx->remote; + struct server *server = remote->server; + if (!remote_send_ctx->connected) { + socklen_t len; + struct sockaddr_storage addr; + len = sizeof addr; + int r = getpeername(remote->fd, (struct sockaddr*)&addr, &len); + if (r == 0) { + remote_send_ctx->connected = 1; + ev_io_stop(EV_A_ &remote_send_ctx->io); + ev_io_start(EV_A_ &server->recv_ctx->io); + ev_io_start(EV_A_ &remote->recv_ctx->io); + } else { + perror("getpeername"); + // not connected + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + } else { + if (remote->buf_len == 0) { + // close and free + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } else { + // has data to send + ssize_t r = send(remote->fd, remote->buf, + remote->buf_len, 0); + if (r < 0) { + perror("send"); + if (errno != EAGAIN) { + // close and free + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + return; + } + if (r < remote->buf_len) { + // partly sent, move memory, wait for the next time to send + char *pt; + for (pt = remote->buf; pt < pt + min(r, BUF_SIZE); pt++) { + *pt = *(pt + r); + } + remote->buf_len -= r; + return; + } else { + // all sent out, wait for reading + ev_io_stop(EV_A_ &remote_send_ctx->io); + if (server != NULL) { + ev_io_start(EV_A_ &server->recv_ctx->io); + } else { + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + } + } - } + } } struct remote* new_remote(int fd) { - struct remote *remote; - remote = malloc(sizeof(struct remote)); - remote->fd = fd; - remote->recv_ctx = malloc(sizeof(struct remote_ctx)); - remote->send_ctx = malloc(sizeof(struct remote_ctx)); - ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ); - ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE); - remote->recv_ctx->remote = remote; - remote->recv_ctx->connected = 0; - remote->send_ctx->remote = remote; - remote->send_ctx->connected = 0; - fprintf(stderr, "new remote\n"); - return remote; + struct remote *remote; + remote = malloc(sizeof(struct remote)); + remote->fd = fd; + remote->recv_ctx = malloc(sizeof(struct remote_ctx)); + remote->send_ctx = malloc(sizeof(struct remote_ctx)); + ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ); + ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE); + remote->recv_ctx->remote = remote; + remote->recv_ctx->connected = 0; + remote->send_ctx->remote = remote; + remote->send_ctx->connected = 0; + fprintf(stderr, "new remote\n"); + return remote; } void free_remote(struct remote *remote) { - if (remote != NULL) { - if (remote->server != NULL) { - remote->server->remote = NULL; - } - free(remote->recv_ctx); - free(remote->send_ctx); - free(remote); - fprintf(stderr, "free remote\n"); - } + if (remote != NULL) { + if (remote->server != NULL) { + remote->server->remote = NULL; + } + free(remote->recv_ctx); + free(remote->send_ctx); + free(remote); + fprintf(stderr, "free remote\n"); + } } void close_and_free_remote(EV_P_ struct remote *remote) { - if (remote != NULL) { - ev_io_stop(EV_A_ &remote->send_ctx->io); - ev_io_stop(EV_A_ &remote->recv_ctx->io); - close(remote->fd); - free_remote(remote); - } + if (remote != NULL) { + ev_io_stop(EV_A_ &remote->send_ctx->io); + ev_io_stop(EV_A_ &remote->recv_ctx->io); + close(remote->fd); + free_remote(remote); + } } struct server* new_server(int fd) { - struct server *server; - server = malloc(sizeof(struct server)); - server->fd = fd; - server->recv_ctx = malloc(sizeof(struct server_ctx)); - server->send_ctx = malloc(sizeof(struct server_ctx)); - ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ); - ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE); - server->recv_ctx->server = server; - server->recv_ctx->connected = 0; - server->send_ctx->server = server; - server->send_ctx->connected = 0; - fprintf(stderr, "new server\n"); - return server; + struct server *server; + server = malloc(sizeof(struct server)); + server->fd = fd; + server->recv_ctx = malloc(sizeof(struct server_ctx)); + server->send_ctx = malloc(sizeof(struct server_ctx)); + ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ); + ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE); + server->recv_ctx->server = server; + server->recv_ctx->connected = 0; + server->send_ctx->server = server; + server->send_ctx->connected = 0; + fprintf(stderr, "new server\n"); + return server; } void free_server(struct server *server) { - if (server != NULL) { - if (server->remote != NULL) { - server->remote->server = NULL; - } - free(server->recv_ctx); - free(server->send_ctx); - free(server); - fprintf(stderr, "free server\n"); - } + if (server != NULL) { + if (server->remote != NULL) { + server->remote->server = NULL; + } + free(server->recv_ctx); + free(server->send_ctx); + free(server); + fprintf(stderr, "free server\n"); + } } void close_and_free_server(EV_P_ struct server *server) { - if (server != NULL) { - ev_io_stop(EV_A_ &server->send_ctx->io); - ev_io_stop(EV_A_ &server->recv_ctx->io); - close(server->fd); - free_server(server); - } + if (server != NULL) { + ev_io_stop(EV_A_ &server->send_ctx->io); + ev_io_stop(EV_A_ &server->recv_ctx->io); + close(server->fd); + free_server(server); + } } static void accept_cb (EV_P_ ev_io *w, int revents) { - struct listen_ctx *listener = (struct listen_ctx *)w; - int serverfd; - while (1) { - serverfd = accept(listener->fd, NULL, NULL); - if (serverfd == -1) { - perror("accept"); - break; - } - setnonblocking(serverfd); - struct server *server = new_server(serverfd); - struct addrinfo hints, *res; - int sockfd; - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - getaddrinfo(_server, _remote_port, &hints, &res); - sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (sockfd < 0) { - perror("socket"); - close(sockfd); - free_server(server); - continue; - } - setnonblocking(sockfd); - struct remote *remote = new_remote(sockfd); - server->remote = remote; - remote->server = server; - connect(sockfd, res->ai_addr, res->ai_addrlen); - freeaddrinfo(res); - // listen to remote connected event - ev_io_start(EV_A_ &remote->send_ctx->io); - break; - } + struct listen_ctx *listener = (struct listen_ctx *)w; + int serverfd; + while (1) { + serverfd = accept(listener->fd, NULL, NULL); + if (serverfd == -1) { + perror("accept"); + break; + } + setnonblocking(serverfd); + struct server *server = new_server(serverfd); + struct addrinfo hints, *res; + int sockfd; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + getaddrinfo(_server, _remote_port, &hints, &res); + sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sockfd < 0) { + perror("socket"); + close(sockfd); + free_server(server); + continue; + } + setnonblocking(sockfd); + struct remote *remote = new_remote(sockfd); + server->remote = remote; + remote->server = server; + connect(sockfd, res->ai_addr, res->ai_addrlen); + freeaddrinfo(res); + // listen to remote connected event + ev_io_start(EV_A_ &remote->send_ctx->io); + break; + } } -int start_local (const char *server, const char *remote_port, - const char *port, const char *key) +int main (int argc, char **argv) { + if (argc < 5) + return -1; + + const char *server = argv[1]; + const char *remote_port = argv[2]; + const char *port = argv[3]; + const char *key = argv[4]; + + /* Our process ID and Session ID */ + pid_t pid, sid; + + /* Fork off the parent process */ + pid = fork(); + if (pid < 0) { + exit(EXIT_FAILURE); + } + /* If we got a good PID, then + we can exit the parent process. */ + if (pid > 0) { + FILE *file = fopen("/data/data/com.github.shadowsocks/shadowsocks.pid", "w"); + fprintf(file, "%d", pid); + fclose(file); + exit(EXIT_SUCCESS); + } + + /* Change the file mode mask */ + umask(0); + + /* Open any logs here */ + + /* Create a new SID for the child process */ + sid = setsid(); + if (sid < 0) { + /* Log the failure */ + exit(EXIT_FAILURE); + } + + + /* Change the current working directory */ + if ((chdir("/")) < 0) { + /* Log the failure */ + exit(EXIT_FAILURE); + } + + /* Close out the standard file descriptors */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + _server = strdup(server); _remote_port = strdup(remote_port); - fprintf(stderr, "calculating ciphers\n"); - get_table(key); + fprintf(stderr, "calculating ciphers\n"); + get_table(key); - int listenfd; - listenfd = create_and_bind(port); - if (listenfd < 0) { - LOGE("bind() error.."); - return 1; - } - if (listen(listenfd, SOMAXCONN) == -1) { - LOGE("listen() error."); - return 1; - } - LOGD("server listening at port %s\n", port); - - setnonblocking(listenfd); - struct listen_ctx listen_ctx; - listen_ctx.fd = listenfd; - struct ev_loop *loop = EV_DEFAULT; - ev_io_init (&listen_ctx.io, accept_cb, listenfd, EV_READ); - ev_io_start (loop, &listen_ctx.io); - ev_run (loop, 0); - return 0; -} - -jint Java_com_github_shadowsocks_Local_main(JNIEnv *env, jobject thiz, jstring server_str, - jstring remote_port_str, jstring port_str, jstring key_str) { - - const char *server = (*env)->GetStringUTFChars(env, server_str, 0); - const char *remote_port = (*env)->GetStringUTFChars(env, remote_port_str, 0); - const char *port = (*env)->GetStringUTFChars(env, port_str, 0); - const char *key = (*env)->GetStringUTFChars(env, key_str, 0); - - int ret = start_local(server, remote_port, port, key); - - (*env)->ReleaseStringUTFChars(env, server_str, server); - (*env)->ReleaseStringUTFChars(env, remote_port_str, remote_port); - (*env)->ReleaseStringUTFChars(env, port_str, port); - (*env)->ReleaseStringUTFChars(env, key_str, key); - - return ret; + int listenfd; + listenfd = create_and_bind(port); + if (listenfd < 0) { + LOGE("bind() error.."); + return 1; + } + if (listen(listenfd, SOMAXCONN) == -1) { + LOGE("listen() error."); + return 1; + } + LOGD("server listening at port %s\n", port); + setnonblocking(listenfd); + struct listen_ctx listen_ctx; + listen_ctx.fd = listenfd; + struct ev_loop *loop = EV_DEFAULT; + ev_io_init (&listen_ctx.io, accept_cb, listenfd, EV_READ); + ev_io_start (loop, &listen_ctx.io); + ev_run (loop, 0); + return 0; } diff --git a/jni/termExec.cpp b/jni/termExec.cpp new file mode 100644 index 00000000..e2c83d2b --- /dev/null +++ b/jni/termExec.cpp @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "Exec" + +#include "jni.h" +#include + +#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0) +#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0) +#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static jclass class_fileDescriptor; +static jfieldID field_fileDescriptor_descriptor; +static jmethodID method_fileDescriptor_init; + +typedef unsigned short char16_t; + +class String8 { +public: + String8() { + mString = 0; + } + + ~String8() { + if (mString) { + free(mString); + } + } + + void set(const char16_t* o, size_t numChars) { + if (mString) { + free(mString); + } + mString = (char*) malloc(numChars + 1); + if (!mString) { + return; + } + for (size_t i = 0; i < numChars; i++) { + mString[i] = (char) o[i]; + } + mString[numChars] = '\0'; + } + + const char* string() { + return mString; + } +private: + char* mString; +}; + +static int throwOutOfMemoryError(JNIEnv *env, const char *message) +{ + jclass exClass; + const char *className = "java/lang/OutOfMemoryError"; + + exClass = env->FindClass(className); + return env->ThrowNew(exClass, message); +} + +static int create_subprocess(const char *cmd, + char *const argv[], char *const envp[], int* pProcessId) +{ + char filename[] = "/data/data/com.github.shadowsocks/defout"; + char script[] = "/data/data/com.github.shadowsocks/script"; + pid_t pid; + + if (chmod(script, 0755) < 0) { + LOGE("error to chmod\n"); + exit(-1); + } + + int defout = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0600); + + if(defout < 0) { + LOGE("open file error\n"); + exit(-1); + } + + pid = fork(); + + if(pid < 0) { + LOGE("- fork failed: %s -\n", strerror(errno)); + return -1; + } + + if(pid == 0){ + + //setsid(); + + dup2(defout, 1); + dup2(defout, 2); + + if (envp) { + for (; *envp; ++envp) { + putenv(*envp); + } + } + + execv(cmd, argv); + exit(-1); + } else { + *pProcessId = (int) pid; + return defout; + } +} + + +static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz, + jstring cmd, jobjectArray args, jobjectArray envVars, + jintArray processIdArray) +{ + const jchar* str = cmd ? env->GetStringCritical(cmd, 0) : 0; + String8 cmd_8; + if (str) { + cmd_8.set(str, env->GetStringLength(cmd)); + env->ReleaseStringCritical(cmd, str); + } + + jsize size = args ? env->GetArrayLength(args) : 0; + char **argv = NULL; + String8 tmp_8; + if (size > 0) { + argv = (char **)malloc((size+1)*sizeof(char *)); + if (!argv) { + throwOutOfMemoryError(env, "Couldn't allocate argv array"); + return NULL; + } + for (int i = 0; i < size; ++i) { + jstring arg = reinterpret_cast(env->GetObjectArrayElement(args, i)); + str = env->GetStringCritical(arg, 0); + if (!str) { + throwOutOfMemoryError(env, "Couldn't get argument from array"); + return NULL; + } + tmp_8.set(str, env->GetStringLength(arg)); + env->ReleaseStringCritical(arg, str); + argv[i] = strdup(tmp_8.string()); + } + argv[size] = NULL; + } + + size = envVars ? env->GetArrayLength(envVars) : 0; + char **envp = NULL; + if (size > 0) { + envp = (char **)malloc((size+1)*sizeof(char *)); + if (!envp) { + throwOutOfMemoryError(env, "Couldn't allocate envp array"); + return NULL; + } + for (int i = 0; i < size; ++i) { + jstring var = reinterpret_cast(env->GetObjectArrayElement(envVars, i)); + str = env->GetStringCritical(var, 0); + if (!str) { + throwOutOfMemoryError(env, "Couldn't get env var from array"); + return NULL; + } + tmp_8.set(str, env->GetStringLength(var)); + env->ReleaseStringCritical(var, str); + envp[i] = strdup(tmp_8.string()); + } + envp[size] = NULL; + } + + int procId; + int ptm = create_subprocess(cmd_8.string(), argv, envp, &procId); + + if (argv) { + for (char **tmp = argv; *tmp; ++tmp) { + free(*tmp); + } + free(argv); + } + if (envp) { + for (char **tmp = envp; *tmp; ++tmp) { + free(*tmp); + } + free(envp); + } + + if (processIdArray) { + int procIdLen = env->GetArrayLength(processIdArray); + if (procIdLen > 0) { + jboolean isCopy; + + int* pProcId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy); + if (pProcId) { + *pProcId = procId; + env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0); + } + } + } + + jobject result = env->NewObject(class_fileDescriptor, method_fileDescriptor_init); + + if (!result) { + LOGE("Couldn't create a FileDescriptor."); + } + else { + env->SetIntField(result, field_fileDescriptor_descriptor, ptm); + } + + return result; +} + + +static int android_os_Exec_waitFor(JNIEnv *env, jobject clazz, + jint procId) { + int status; + waitpid(procId, &status, 0); + int result = 0; + if (WIFEXITED(status)) { + result = WEXITSTATUS(status); + } + return result; +} + +static void android_os_Exec_close(JNIEnv *env, jobject clazz, jobject fileDescriptor) +{ + int fd; + + fd = env->GetIntField(fileDescriptor, field_fileDescriptor_descriptor); + + if (env->ExceptionOccurred() != NULL) { + return; + } + + close(fd); +} + +static void android_os_Exec_hangupProcessGroup(JNIEnv *env, jobject clazz, + jint procId) { + kill(-procId, SIGHUP); +} + + +static int register_FileDescriptor(JNIEnv *env) +{ + jclass localRef_class_fileDescriptor = env->FindClass("java/io/FileDescriptor"); + + if (localRef_class_fileDescriptor == NULL) { + LOGE("Can't find class java/io/FileDescriptor"); + return -1; + } + + class_fileDescriptor = (jclass) env->NewGlobalRef(localRef_class_fileDescriptor); + + env->DeleteLocalRef(localRef_class_fileDescriptor); + + if (class_fileDescriptor == NULL) { + LOGE("Can't get global ref to class java/io/FileDescriptor"); + return -1; + } + + field_fileDescriptor_descriptor = env->GetFieldID(class_fileDescriptor, "descriptor", "I"); + + if (field_fileDescriptor_descriptor == NULL) { + LOGE("Can't find FileDescriptor.descriptor"); + return -1; + } + + method_fileDescriptor_init = env->GetMethodID(class_fileDescriptor, "", "()V"); + if (method_fileDescriptor_init == NULL) { + LOGE("Can't find FileDescriptor.init"); + return -1; + } + return 0; +} + + +static const char *classPathName = "com/github/shadowsocks/Exec"; + +static JNINativeMethod method_table[] = { + { "createSubprocess", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[I)Ljava/io/FileDescriptor;", + (void*) android_os_Exec_createSubProcess }, + { "waitFor", "(I)I", + (void*) android_os_Exec_waitFor}, + { "close", "(Ljava/io/FileDescriptor;)V", + (void*) android_os_Exec_close}, + { "hangupProcessGroup", "(I)V", + (void*) android_os_Exec_hangupProcessGroup} +}; + +/* + * Register several native methods for one class. + */ +static int registerNativeMethods(JNIEnv* env, const char* className, + JNINativeMethod* gMethods, int numMethods) +{ + jclass clazz; + + clazz = env->FindClass(className); + if (clazz == NULL) { + LOGE("Native registration unable to find class '%s'", className); + return JNI_FALSE; + } + if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { + LOGE("RegisterNatives failed for '%s'", className); + return JNI_FALSE; + } + + return JNI_TRUE; +} + +/* + * Register native methods for all classes we know about. + * + * returns JNI_TRUE on success. + */ +static int registerNatives(JNIEnv* env) +{ + if (!registerNativeMethods(env, classPathName, method_table, + sizeof(method_table) / sizeof(method_table[0]))) { + return JNI_FALSE; + } + + return JNI_TRUE; +} + + +// ---------------------------------------------------------------------------- + +/* + * This is called by the VM when the shared library is first loaded. + */ + +typedef union { + JNIEnv* env; + void* venv; +} UnionJNIEnvToVoid; + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + UnionJNIEnvToVoid uenv; + uenv.venv = NULL; + jint result = -1; + JNIEnv* env = NULL; + + LOGI("JNI_OnLoad"); + + if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) { + LOGE("ERROR: GetEnv failed"); + goto bail; + } + env = uenv.env; + + if ((result = register_FileDescriptor(env)) < 0) { + LOGE("ERROR: registerFileDescriptor failed"); + goto bail; + } + + if (registerNatives(env) != JNI_TRUE) { + LOGE("ERROR: registerNatives failed"); + goto bail; + } + + result = JNI_VERSION_1_4; + +bail: + return result; +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 00000000..a412dca7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,222 @@ + + + 4.0.0 + com.github.shadowsocks + shadowsocks + 1.0 + apk + Shadowsocks + + + + madeye-maven-repository + http://madeye-maven-repository.googlecode.com/git + + true + + + false + + + + + + + UTF-8 + + + + + + com.google.android + android + 4.1.1.4 + provided + + + + com.google.android.analytics + analytics + V2 + + + + commons-codec + commons-codec + 1.5 + + + + + ${project.artifactId} + src + + + + maven-jarsigner-plugin + 1.2 + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + 3.4.1 + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + true + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.5 + + + org.apache.maven.plugins + maven-jarsigner-plugin + + + signing + + sign + + package + true + + + + ${project.build.directory}/${project.artifactId}.apk + + ${sign.keystore} + ${sign.alias} + ${sign.storepass} + ${sign.keypass} + true + + -sigalgMD5withRSA + -digestalgSHA1 + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + + + alignApk + install + + zipalign + + + + true + + true + + false + + + true + false + ${project.build.directory}/${project.artifactId}.apk + ${project.build.directory}/${project.artifactId}-${project.version}.apk + + + + 16 + + + + + + + + debug-sign + + + ${basedir}/local.properties + + + + ${basedir}/travis.keystore + travis + travis + travis + + + + release-sign + + + ${basedir}/local.properties + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0-alpha-2 + + + initialize + + read-project-properties + + + + ${basedir}/local.properties + + + + + + + + + + travis + + + TRAVIS + true + + + + + + org.codehaus.mojo + exec-maven-plugin + + + deploy-to-sae + install + + exec + + + + + ${basedir}/deploy + + ${project.build.directory} + ${project.artifactId}-${project.version}.apk + + + + + + + + diff --git a/res/drawable-hdpi-v11/ic_stat_gaeproxy.png b/res/drawable-hdpi-v11/ic_stat_gaeproxy.png new file mode 100755 index 00000000..87d80079 Binary files /dev/null and b/res/drawable-hdpi-v11/ic_stat_gaeproxy.png differ diff --git a/res/drawable-hdpi-v9/ic_stat_gaeproxy.png b/res/drawable-hdpi-v9/ic_stat_gaeproxy.png new file mode 100755 index 00000000..ff7dd684 Binary files /dev/null and b/res/drawable-hdpi-v9/ic_stat_gaeproxy.png differ diff --git a/res/drawable-hdpi/ic_stat_gaeproxy.png b/res/drawable-hdpi/ic_stat_gaeproxy.png new file mode 100755 index 00000000..7915f888 Binary files /dev/null and b/res/drawable-hdpi/ic_stat_gaeproxy.png differ diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png new file mode 100644 index 00000000..80cdaa7d Binary files /dev/null and b/res/drawable-hdpi/icon.png differ diff --git a/res/drawable-hdpi/ing.png b/res/drawable-hdpi/ing.png new file mode 100644 index 00000000..42ff8f38 Binary files /dev/null and b/res/drawable-hdpi/ing.png differ diff --git a/res/drawable-hdpi/off.png b/res/drawable-hdpi/off.png new file mode 100644 index 00000000..14670b7f Binary files /dev/null and b/res/drawable-hdpi/off.png differ diff --git a/res/drawable-hdpi/on.png b/res/drawable-hdpi/on.png new file mode 100644 index 00000000..53e623fb Binary files /dev/null and b/res/drawable-hdpi/on.png differ diff --git a/res/drawable-ldpi-v11/ic_stat_gaeproxy.png b/res/drawable-ldpi-v11/ic_stat_gaeproxy.png new file mode 100755 index 00000000..0682f157 Binary files /dev/null and b/res/drawable-ldpi-v11/ic_stat_gaeproxy.png differ diff --git a/res/drawable-ldpi-v9/ic_stat_gaeproxy.png b/res/drawable-ldpi-v9/ic_stat_gaeproxy.png new file mode 100755 index 00000000..acb9f873 Binary files /dev/null and b/res/drawable-ldpi-v9/ic_stat_gaeproxy.png differ diff --git a/res/drawable-ldpi/ic_stat_gaeproxy.png b/res/drawable-ldpi/ic_stat_gaeproxy.png new file mode 100755 index 00000000..5d24f04f Binary files /dev/null and b/res/drawable-ldpi/ic_stat_gaeproxy.png differ diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png new file mode 100644 index 00000000..566cd440 Binary files /dev/null and b/res/drawable-ldpi/icon.png differ diff --git a/res/drawable-ldpi/ing.png b/res/drawable-ldpi/ing.png new file mode 100644 index 00000000..7305012b Binary files /dev/null and b/res/drawable-ldpi/ing.png differ diff --git a/res/drawable-ldpi/off.png b/res/drawable-ldpi/off.png new file mode 100644 index 00000000..f6c5098f Binary files /dev/null and b/res/drawable-ldpi/off.png differ diff --git a/res/drawable-ldpi/on.png b/res/drawable-ldpi/on.png new file mode 100644 index 00000000..65ad4c5b Binary files /dev/null and b/res/drawable-ldpi/on.png differ diff --git a/res/drawable-mdpi-v11/ic_stat_gaeproxy.png b/res/drawable-mdpi-v11/ic_stat_gaeproxy.png new file mode 100755 index 00000000..1e36c79b Binary files /dev/null and b/res/drawable-mdpi-v11/ic_stat_gaeproxy.png differ diff --git a/res/drawable-mdpi-v9/ic_stat_gaeproxy.png b/res/drawable-mdpi-v9/ic_stat_gaeproxy.png new file mode 100755 index 00000000..46fd0ad8 Binary files /dev/null and b/res/drawable-mdpi-v9/ic_stat_gaeproxy.png differ diff --git a/res/drawable-mdpi/ic_stat_gaeproxy.png b/res/drawable-mdpi/ic_stat_gaeproxy.png new file mode 100755 index 00000000..aaba1b9c Binary files /dev/null and b/res/drawable-mdpi/ic_stat_gaeproxy.png differ diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png new file mode 100644 index 00000000..e329c02b Binary files /dev/null and b/res/drawable-mdpi/icon.png differ diff --git a/res/drawable-mdpi/ing.png b/res/drawable-mdpi/ing.png new file mode 100644 index 00000000..e18492d2 Binary files /dev/null and b/res/drawable-mdpi/ing.png differ diff --git a/res/drawable-mdpi/off.png b/res/drawable-mdpi/off.png new file mode 100644 index 00000000..f5053f88 Binary files /dev/null and b/res/drawable-mdpi/off.png differ diff --git a/res/drawable-mdpi/on.png b/res/drawable-mdpi/on.png new file mode 100644 index 00000000..83326e95 Binary files /dev/null and b/res/drawable-mdpi/on.png differ diff --git a/res/drawable-xhdpi-v11/ic_stat_gaeproxy.png b/res/drawable-xhdpi-v11/ic_stat_gaeproxy.png new file mode 100755 index 00000000..e319e267 Binary files /dev/null and b/res/drawable-xhdpi-v11/ic_stat_gaeproxy.png differ diff --git a/res/drawable-xhdpi-v9/ic_stat_gaeproxy.png b/res/drawable-xhdpi-v9/ic_stat_gaeproxy.png new file mode 100755 index 00000000..de3e66a2 Binary files /dev/null and b/res/drawable-xhdpi-v9/ic_stat_gaeproxy.png differ diff --git a/res/drawable-xhdpi/ic_stat_gaeproxy.png b/res/drawable-xhdpi/ic_stat_gaeproxy.png new file mode 100755 index 00000000..e68bccfd Binary files /dev/null and b/res/drawable-xhdpi/ic_stat_gaeproxy.png differ diff --git a/res/layout/gaeproxy_appwidget.xml b/res/layout/gaeproxy_appwidget.xml new file mode 100644 index 00000000..75d19fea --- /dev/null +++ b/res/layout/gaeproxy_appwidget.xml @@ -0,0 +1,8 @@ + + + + diff --git a/res/layout/layout_apps.xml b/res/layout/layout_apps.xml new file mode 100644 index 00000000..e9ce741c --- /dev/null +++ b/res/layout/layout_apps.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/res/layout/layout_apps_item.xml b/res/layout/layout_apps_item.xml new file mode 100644 index 00000000..ad821a9e --- /dev/null +++ b/res/layout/layout_apps_item.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/res/layout/layout_progress_bar.xml b/res/layout/layout_progress_bar.xml new file mode 100644 index 00000000..195e7f62 --- /dev/null +++ b/res/layout/layout_progress_bar.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100755 index 00000000..3c196e63 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/overlay.xml b/res/layout/overlay.xml new file mode 100644 index 00000000..49c1b45f --- /dev/null +++ b/res/layout/overlay.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/res/values-zh/strings.xml b/res/values-zh/strings.xml new file mode 100755 index 00000000..298afbad --- /dev/null +++ b/res/values-zh/strings.xml @@ -0,0 +1,537 @@ + + + + 正通过Shadowsocks连接到互联网 + Shadowsocks + Shadowsocks启用成功 + Shadowsocks启用失败 + Shadowsocks服务已停止 + 恢复连接中 + 恢复连接成功 + 服务已关闭 + 服务已启动 + Shadowsocks连接已关闭 + 请检查你的网络状况和账户信息 + 恢复连接失败 + 我知道了 + 代理地址 + 本地端口 + 密钥 + 自动连接 + 自动恢复连接 + 全局代理 + 关闭服务 + 启动服务 + 反馈 @ofmax + Shadowsocks设置 + 功能设置 + 后台服务 + 代理开关 + 开启 / 关闭后台服务 + Shadowsocks服务地址 + 本地代理服务器监听端口,任意大于1024的数字 + 加密密钥 + 端口必需是大于1024的数字 + 启动时自动打开代理服务 + 配置全局代理,需要 ROOT 权限以及 IPTABLES 的支持 + 端口不能为空 + 代理地址不能为空 + 模块管理 + 安装/卸载 + 安装/卸载依赖模块,请确认SD卡已插入,并连接到互联网 + 下载中... + 下载中...(100%处请耐心等待) + 请先安装依赖模块,滑动到底部,选择安装 + 请确认SD卡已插入 + 重置 + 解压缩中... + 分应用代理 + 为应用单独设置代理,需要 ROOT 权限以及 IPTABLES 的支持 + 白名单模式 + 选择的应用将不通过代理 + 选择使用Shadowsocks的应用: + 检测到一次非正常退出,状态已重置 + Shadowsocks是一款开源软件,依照GPLv3协议发布。\n\n如果您有任何问题,请前往项目网站进行反馈。 + \n\n(gaeproxy.googlecode.com) + 关于 + 代理类型 + 正在连接... + 正在初始化... + 正在重置... + 请修改你的代理地址,以 "https://" 开头 + HTTPS代理 + 重定向HTTPS流量,若遇到反复尝试无法连接的情况,请关闭此选项 + + + 通知设置 + 铃声 + 选择通知的铃声 + 震动 + 在连接状态改变时震动 + 正在加载应用列表... + 电子市场代理 + 帮助大陆用户从电子市场中获得程序更新(需要重启) + 国内路由 + 访问国内站点时忽略本地代理(实验性) + Shadowsocks正在启动 + Shadowsocks正在重新启动 + 内置浏览器 + 启动内置代理浏览器, 只针对没有ROOT权限或IPTABLES支持的用户 + 您正在使用默认的代理地址,此地址每天只有10G的带宽配额,也就说您将随时无法正常使用本应用\n\n因此,请尽快部署自己的服务器端,具体步骤和说明见:http://code.google.com/p/goagent/ + 警告 + + 系统代理 + 设置系统代理,无需ROOT权限,仅工作在Android 4.0及以上系统,并仅对当前Wifi连接生效 + + + + Zirco Browser + Zirco Browser - %s + 使用Zirco Browser让你更好的使用互联网 + 关于 + 版权: + http://code.google.com/p/zirco-browser/ + GPL v3 + 添加书签 + 书签 + 历史 + 下载 + 配置 + 打开 + 在新页中打开 + 下载 + 下载开始 + 下载结束 + 下载错误: %s + 页 %s + 页 %1$s: %2$s + 不能打开网址 + 本程序不支持这个网址: %s. + 书签 + 排序方式 + 增加书签 + 没有书签 + 打开新页 + 编辑书签 + 删除书签 + 导入书签 + 导出书签 + 清空所有书签 + 正在导入书签中... + 重在导出书签中... + 正在清空书签中... + 顺序 + 倒序 + 选择导入源 + Android浏览器的书签 + 选择导出目标 + 编辑书签 + 增加书签 + 显示名称: + 地址Location: + 历史 + 打开新页 + 删除 + Zirco Browser-下载 + 没有下载 + %s (取消) + %s (完成) + Zirco Browser - Ad 白名单 + 白名单是空的. + 添加至白名单 + 清空白名单 + 通用设置 + 主页设定 + 定义你的默认主页 + 是否全屏幕 + 在全屏幕模式下显示 (需要重新启动程序). + 页切换时是否显示对话框 + 如果选中,也切换时会有提示 + 浏览器设定 + 历史记录大小 + 每天历史记录大小 + 是否支持JavaScript + 如果不支持JavaScript,很多网站效果不能打开,建议打开. + 是否加载图像 + 使用或者不使用图像 + 隐私设定 + 管理你的上网痕迹,确保隐私 + 保存表单数据 + 可保存数据,减少你下次输入时间 + 保存密码 + 可保存密码,让你下次访问更加方便 + 允许cookie + 是否允许Cookie + 清空历史 + 清空表单数据 + 清空缓存 + 清空cookies + 工具条显示时间 + 显示工具条多长时间自动隐藏 + 1 秒 + 2 秒 + 3 秒 + 4 秒 + 5 秒 + + + @string/PreferencesActivity.BarDuration1sec + @string/PreferencesActivity.BarDuration2sec + @string/PreferencesActivity.BarDuration3sec + @string/PreferencesActivity.BarDuration4sec + @string/PreferencesActivity.BarDuration5sec + + + 1000 + 2000 + 3000 + 4000 + 5000 + + + 工具条图标 + 工具条图标位置 + 右 + 左 + 两边都有 + + + @string/PreferencesActivity.BubbleRight + @string/PreferencesActivity.BubbleLeft + @string/PreferencesActivity.BubbleBoth + + + right + left + both + + + Ad广告设定 + 是否允许 Ad-blocker + Ad广告必须要求JavaScript能使用 + 白名单 + 管理 Ad广告白名单. + 确定 + 取消 + 请等待 + 是 + 否 + 操作没有完成,是否继续? + 今天 (%s) + 昨天 (%s) + %1$s 以前 (%2$s) + JavaScript对话框 + 清空历史记录 + 正在清空历史记录... + 清空表单数据 + 正在清空表单数据... + 清空缓存 + 正在清空缓存... + 清空Cookies + 正在清空缓存... + SD卡不能使用 + 找不到SD卡 + SD卡不能使用 + 关闭 + yyyy-MM-dd\'T\'HH:mm:ss.SSS + + + 关于 + 关于 + 本程序信息. + + + 更多.. + 默认缩放级别 + 设定默认缩放 + 关闭 + 大 + 非常大 + + + @string/PreferencesActivity.DefaultZoomClose + @string/PreferencesActivity.DefaultZoomMedium + @string/PreferencesActivity.DefaultZoomFar + + + CLOSE + MEDIUM + FAR + + + 欢迎! + 使用最多的书签 + 历史记录 + 更新历史 + 更新日志 + 本应用程序更新日志 + 主页 + 主页 + 开始页 + 空白页 + 自定义 + + + @string/PreferencesActivity.HomepageStart + @string/PreferencesActivity.HomepageBlank + @string/PreferencesActivity.HomepageCustom + + + + http://www.google.com/m?hl=en&gl=us&client=ms-null&source=android-browser-key&q=%s + http://www.baidu.com/s?wd=%s + 搜索 URL + 自定义搜索URL + 搜索 + 搜索 + 谷歌 + 百度 + 自定义 + + + @string/PreferencesActivity.SearchUrlGoogle + @string/PreferencesActivity.SearchUrlWikipedia + @string/PreferencesActivity.SearchUrlCustom + + + 搜索 + 搜 + 开始页设定 + 自定义开始页. + 是否显示搜索栏 + 如果选中,首页显示搜索栏 + 显示最常用书签 + 如果选中,首页显示最常用书签 + 是否显示历史 + 如果选中,显示浏览历史 + 书签个数 + 首页中最常用的书签显示 + 当前历史记录数 + 在首页中显示的历史记录个数 + User agent(UA) + 定义UA,网站根据UA显示给适合你浏览器的网页 + User Agent + User agent + Android默认 + 桌面级 + 自定义 + + + @string/PreferencesActivity.UserAgentDefault + @string/PreferencesActivity.UserAgentDesktop + @string/PreferencesActivity.UserAgentCustom + + + Android浏览器的书签 + SD卡 + 导出完成 + 你的所有书签已经保存在[%s]中 + 导出失败 + 错误信息: %s. + 复制链接地址 + 显示图片 + 复制图片地址 + 下载图片 + 发送邮件 + 复制邮件地址 + 地址已经复制到粘贴板 + 下载开始. + 正在下载中. + 下载完成. + 下载已经取消. + 复制URL + 清空缓存 + 如果选中,当程序退出的时候清空缓存. + 自定义首页 + 自定义首页界面 + 退出 + 需要重启 + 效果改变后需要重新启动程序,是否马上重启 + + + 可以使用插件 + 关闭 + 需要的时候提示 + 打开 + + + @string/PreferencesActivity.PluginsAlwaysOff + @string/PreferencesActivity.PluginsOnDemand + @string/PreferencesActivity.PluginsAlwaysOn + + + OFF + ON_DEMAND + ON + + + 继续 + 安全警告 + 网站的安全证书存在问题 + 网站证书没有可信任的发行者 + 站点名和证书不匹配 + 证书已经过期 + 证书没有验证 + Zirco Browser-通过网关访问的网站列表 + 列表为空 + 添加一个网站 + 清空所有站点 + 移动网站列表 + 管理网站 + 通过google移动平台访问网站,会减少流量和加快速度. + 增加 + 清空 + 删除 + + + 声音键 + 选择声音键的功能 + 默认不改变 + 放大 + 选择不同页 + 前进/后退 + + + @string/PreferencesActivity.VolumeKeysActionDefault + @string/PreferencesActivity.VolumeKeysActionZoom + @string/PreferencesActivity.VolumeKeysActionSwitchTabs + @string/PreferencesActivity.VolumeKeysActionHistory + + + DEFAULT + ZOOM + SWITCH_TABS + HISTORY + + + + Share page + Share via + Share link url + Share image url + Share email address + + + Hide title bars + If checked, windows title bars will not be + displayed (need application restart). + Use wide viewport + If checked, browser will use a viewport similar to + desktop browsers. + Load pages with overview + If checked, pages will load zoomed out to show an + overview of the page. + Switch tabs method + Define how you can switch between open tabs. + Buttons + Fling + Both + + + @string/PreferencesActivity.SwitchTabButtons + @string/PreferencesActivity.SwitchTabFling + @string/PreferencesActivity.SwitchTabBoth + + + buttons + fling + both + + + + Today + Yesterday + Last 7 days + Last month + Older + 主页 + 菜单 + 分享 + 选择文本 + 移动网关 + This application include code from the GreenDroid + project (https://github.com/cyrilmottier/GreenDroid/), licensed under + the Apache license, version 2. + + + Firefox bookmarks synchronization + Manage the settings of the bookmarks + synchronization with Firefox Sync. + Use Firefox Sync + If checked, you can retrieve your bookmarks from + Firefox Sync. + Server + Set the Firefox Sync server. + User name + Enter your Firefox Sync user name. + Password + Enter your Firefox Sync password. + Key + Enter your Firefox Sync key. + Firefox bookmarks + Synchronize + Clear + Places + Firefox bookmarks are not synchronized. Setup your + account informations and do a synchronization. + No bookmarks in this folder. + Setup + Synchronize + Synchronization + Connecting to server... + Getting data... + Reading data: %1$s / %2$s + Writting data... + Synchronization failed. + Synchronization failed: %s + Invalid authentication token. Check your settings. + Server + Server + Firefox Sync server + Custom server + + + @string/WeaveServerPreferenceActivity.DefaultServer + @string/WeaveServerPreferenceActivity.CustomServer + + + Choose file for upload + Export history and bookmarks + Export done + Your history and bookmarks have been saved to %s. + Export failed + Error message: %s. + Import failed + This operation can take some times. Do you wish to + proceed? + Tools + Manage bookmarks and history + Import, export, clear history and bookmarks. + Export bookmarks and history + Export bookmarks and history to the SD card. + Import bookmarks and history + Import bookmarks and history from the SD card. + Clear history and/or bookmarks + Use with caution, this operation cannot be undone. + Clearing history and/or bookmarks... + Clear history and/or bookmarks + Bookmarks + History + All + Exporting history and bookmarks... + Importing history and bookmarks... + Choose the import source + 正在清空书签中... + Remove completed downloads + Find on page + Find on page + + diff --git a/res/values/analytics.xml b/res/values/analytics.xml new file mode 100755 index 00000000..6de05dfe --- /dev/null +++ b/res/values/analytics.xml @@ -0,0 +1,11 @@ + + + + UA-33136904-1 + + + true + + + true + \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 00000000..f3cb82f5 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,26 @@ + + + + + + #ff5e5e5e + #17170443 + #000000 + #646464 + #c8c8c8 + #323232 + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100755 index 00000000..309d28e5 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,169 @@ + + + + Running in the background. + Shadowsocks + Service started. + Service failed to start. + Shadowsocks service stopped. + Auto Reconnected + Reconnected successfully. + Service has been stopped already. + Service has been started already. + Proxy Service Stopped + Please check your network and login information + Cannot reconnect + OK, I know + Address + Local Port + Site Key + Auto Connect + Auto reconnect + Global Proxy + Disconnect + Connect + Feedback @ofmax + Shadowsocks Settings + Feature Settings + Proxy Switch + Service Controller + Enable / Disable Proxy + Shadowsocks URL + Listening port + Site key for GoAgent + The port number should be greater than 1024 + Start Shadowsocks at the startup + Set up the global proxy (needs ROOT permission and + IPTABLES support) + + Port should not be empty + Proxy should not be empty + Package configure + Install / Uninstall + Downloading + Downloading, Patient Please... + Install / Uninstall extra packages (need SD card) + Please install dependent module first! (Scroll to + the bottom, and select to install) + + Please make sure you have a SD CARD! + Reset + Unzipping... + Individual Proxy + Set individual proxy for apps (needs ROOT permission and IPTABLES support) + + Bypass Mode + Enable this option to bypass selected apps + Select apps to use with Shadowsocks: + An unexpected exit detected, context has been + recovered + + Shadowsocks is an open source project + published under + the GPLv3. \n\nIf you have any questions, please visit + our project page on + googlecode. \n\n(gaeproxy.googlecode.com) + + About + Proxy Type + + GAE + PaaS + + + + 209.0.0.0/8 + 72.0.0.0/8 + 74.0.0.0/8 + 68.0.0.0/8 + 69.0.0.0/8 + 66.0.0.0/8 + 64.0.0.0/8 + 199.0.0.0/8 + 168.0.0.0/8 + 67.0.0.0/8 + 173.0.0.0/8 + 202.248.0.0/16 + + + 1.0.0.0/8 + 14.0.0.0/8 + 27.0.0.0/8 + 36.0.0.0/8 + 39.0.0.0/8 + 42.0.0.0/8 + 49.0.0.0/8 + 58.0.0.0/7 + 60.0.0.0/7 + 96.0.0.0/3 + 128.0.0.0/4 + 171.0.0.0/8 + 175.0.0.0/8 + 180.0.0.0/8 + 182.0.0.0/8 + 183.0.0.0/8 + 202.0.0.0/8 + 203.0.0.0/8 + 210.0.0.0/8 + 211.0.0.0/8 + 216.0.0.0/5 + 192.168.0.0/16 + 10.0.0.0/8 + 172.16.0.0/12 + + + + d3dxZ3R4eHByb3h5LTF8d3dxZ3R4eHByb3h5LTJ8d3dxZ3R4eHByb3h5LTN8d3dxZ3R4eHByb3h5LTR8d3dxZ3R4eHByb3h5LTV8d3dxZ3R4eHByb3h5LTZ8d3dxZ3R4eHByb3h5LTd8d3dxZ3R4eHByb3h5LTh8d3dxZ3R4eHByb3h5LTl8d3dxZ3R4eHByb3h5LTEwfHd3cWd0eHhwcm94eTEtMXx3d3FndHh4cHJveHkxLTJ8d3dxZ3R4eHByb3h5MS0zfHd3cWd0eHhwcm94eTEtNHx3d3FndHh4cHJveHkxLTV8d3dxZ3R4eHByb3h5MS02fHd3cWd0eHhwcm94eTEtN3x3d3FndHh4cHJveHkxLTh8d3dxZ3R4eHByb3h5MS05fHd3cWd0eHhwcm94eTEtMTB8d3dxZ3R4eHByb3h5Mi0xfHd3cWd0eHhwcm94eTItMnx3d3FndHh4cHJveHkyLTN8d3dxZ3R4eHByb3h5Mi00fHd3cWd0eHhwcm94eTItNXx3d3FndHh4cHJveHkyLTZ8d3dxZ3R4eHByb3h5Mi03fHd3cWd0eHhwcm94eTItOHx3d3FndHh4cHJveHkyLTl8d3dxZ3R4eHByb3h5Mi0xMA== + + 2 + wwqgtxx-goagent + + Connecting… + Initializing… + Reseting… + Please modify your proxy host to start with + "https://" + + HTTPS Proxy + Proxy HTTPS through GAE, not compatible with most + apps + + + + Notification Settings + Ringtone + Select the notification\'s ringtone + Vibrate + Also vibrate when connection status changes + Loading app list… + Market Proxy + Only for users in China, help to update app from + Android Market (need reboot) + + CHN Route + Bypass all sites located in China (needs ROOT + permission and + IPTABLES support) + + Connecting to Shadowsocks + Reconnecting to Shadowsocks + GAE Browser + Start a browser through GAE proxy, for users + without root permission or iptables support + + You are using the default proxy, which has a quota + of only 10G bandwidth every day. Please deploy your own + servers to get + more free bandwidth quota with Google App Engine.\n\nYou can find more + information here: https://github.com/phus/goagent + + Warning + + System Proxy + Enable system wide proxy automatically without ROOT permission, + only works for Wifi on Android 4.0 and above. + + Remote Port + Server port of shadowsocks + + diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100755 index 00000000..d4d1aa2e --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/shadowsocks_preference.xml b/res/xml/shadowsocks_preference.xml new file mode 100755 index 00000000..d8bfb8df --- /dev/null +++ b/res/xml/shadowsocks_preference.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/github/shadowsocks/AppManager.java b/src/com/github/shadowsocks/AppManager.java new file mode 100755 index 00000000..f331864c --- /dev/null +++ b/src/com/github/shadowsocks/AppManager.java @@ -0,0 +1,416 @@ +/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ +/* See LICENSE for licensing information */ + +package com.github.shadowsocks; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; +import android.widget.*; +import android.widget.AbsListView.OnScrollListener; +import android.widget.CompoundButton.OnCheckedChangeListener; + +import java.util.*; + +public class AppManager extends Activity implements OnCheckedChangeListener, + OnClickListener { + + private static class ListEntry { + private CheckBox box; + private TextView text; + private ImageView icon; + } + + public static ProxyedApp[] getProxyedApps(Context context) { + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + + String tordAppString = prefs.getString(PREFS_KEY_PROXYED, ""); + String[] tordApps; + + StringTokenizer st = new StringTokenizer(tordAppString, "|"); + tordApps = new String[st.countTokens()]; + int tordIdx = 0; + while (st.hasMoreTokens()) { + tordApps[tordIdx++] = st.nextToken(); + } + + Arrays.sort(tordApps); + + // else load the apps up + PackageManager pMgr = context.getPackageManager(); + + List lAppInfo = pMgr.getInstalledApplications(0); + + Iterator itAppInfo = lAppInfo.iterator(); + + Vector vectorApps = new Vector(); + + ApplicationInfo aInfo = null; + + while (itAppInfo.hasNext()) { + aInfo = itAppInfo.next(); + + // ignore system apps + if (aInfo.uid < 10000) + continue; + + ProxyedApp app = new ProxyedApp(); + + app.setUid(aInfo.uid); + + app.setUsername(pMgr.getNameForUid(app.getUid())); + + // check if this application is allowed + if (aInfo.packageName != null + && aInfo.packageName.equals("com.github.shadowsocks")) { + app.setProxyed(true); + } else if (Arrays.binarySearch(tordApps, app.getUsername()) >= 0) { + app.setProxyed(true); + } else { + app.setProxyed(false); + } + + vectorApps.add(app); + + } + + ProxyedApp[] apps = new ProxyedApp[vectorApps.size()]; + vectorApps.toArray(apps); + return apps; + } + + private ProxyedApp[] apps = null; + + private ListView listApps; + + private AppManager mAppManager; + private TextView overlay; + + private ProgressDialog pd = null; + private ListAdapter adapter; + + private ImageLoader dm; + + private static final int MSG_LOAD_START = 1; + + private static final int MSG_LOAD_FINISH = 2; + + public final static String PREFS_KEY_PROXYED = "Proxyed"; + + private boolean appsLoaded = false; + + final Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOAD_START: + pd = ProgressDialog.show(AppManager.this, "", + getString(R.string.loading), true, true); + break; + case MSG_LOAD_FINISH: + + listApps.setAdapter(adapter); + + listApps.setOnScrollListener(new OnScrollListener() { + + boolean visible; + + @Override + public void onScroll(AbsListView view, + int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (visible) { + String name = apps[firstVisibleItem].getName(); + if (name != null && name.length() > 1) + overlay.setText(apps[firstVisibleItem] + .getName().substring(0, 1)); + else + overlay.setText("*"); + overlay.setVisibility(View.VISIBLE); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, + int scrollState) { + visible = true; + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + overlay.setVisibility(View.INVISIBLE); + } + } + }); + + if (pd != null) { + pd.dismiss(); + pd = null; + } + break; + } + super.handleMessage(msg); + } + }; + + public void getApps(Context context) { + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + + String tordAppString = prefs.getString(PREFS_KEY_PROXYED, ""); + String[] tordApps; + + StringTokenizer st = new StringTokenizer(tordAppString, "|"); + tordApps = new String[st.countTokens()]; + int tordIdx = 0; + while (st.hasMoreTokens()) { + tordApps[tordIdx++] = st.nextToken(); + } + + Arrays.sort(tordApps); + + Vector vectorApps = new Vector(); + + // else load the apps up + PackageManager pMgr = context.getPackageManager(); + + List lAppInfo = pMgr.getInstalledApplications(0); + + Iterator itAppInfo = lAppInfo.iterator(); + + ApplicationInfo aInfo = null; + + while (itAppInfo.hasNext()) { + aInfo = itAppInfo.next(); + + // ignore system apps + if (aInfo.uid < 10000) + continue; + + if (aInfo.processName == null) + continue; + if (pMgr.getApplicationLabel(aInfo) == null + || pMgr.getApplicationLabel(aInfo).toString().equals("")) + continue; + if (pMgr.getApplicationIcon(aInfo) == null) + continue; + + ProxyedApp tApp = new ProxyedApp(); + + tApp.setEnabled(aInfo.enabled); + tApp.setUid(aInfo.uid); + tApp.setUsername(pMgr.getNameForUid(tApp.getUid())); + tApp.setProcname(aInfo.processName); + tApp.setName(pMgr.getApplicationLabel(aInfo).toString()); + + // check if this application is allowed + if (Arrays.binarySearch(tordApps, tApp.getUsername()) >= 0) { + tApp.setProxyed(true); + } else { + tApp.setProxyed(false); + } + + vectorApps.add(tApp); + } + + apps = new ProxyedApp[vectorApps.size()]; + vectorApps.toArray(apps); + + } + + private void loadApps() { + getApps(this); + + Arrays.sort(apps, new Comparator() { + @Override + public int compare(ProxyedApp o1, ProxyedApp o2) { + if (o1 == null || o2 == null || o1.getName() == null + || o2.getName() == null) + return 1; + if (o1.isProxyed() == o2.isProxyed()) + return o1.getName().compareTo(o2.getName()); + if (o1.isProxyed()) + return -1; + return 1; + } + }); + + final LayoutInflater inflater = getLayoutInflater(); + + adapter = new ArrayAdapter(this, R.layout.layout_apps_item, + R.id.itemtext, apps) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ListEntry entry; + if (convertView == null) { + // Inflate a new view + convertView = inflater.inflate(R.layout.layout_apps_item, + parent, false); + entry = new ListEntry(); + entry.icon = (ImageView) convertView + .findViewById(R.id.itemicon); + entry.box = (CheckBox) convertView + .findViewById(R.id.itemcheck); + entry.text = (TextView) convertView + .findViewById(R.id.itemtext); + + entry.text.setOnClickListener(mAppManager); + entry.text.setOnClickListener(mAppManager); + + convertView.setTag(entry); + + entry.box.setOnCheckedChangeListener(mAppManager); + } else { + // Convert an existing view + entry = (ListEntry) convertView.getTag(); + } + + final ProxyedApp app = apps[position]; + + entry.icon.setTag(app.getUid()); + + dm.DisplayImage(app.getUid(), + (Activity) convertView.getContext(), entry.icon); + + entry.text.setText(app.getName()); + + final CheckBox box = entry.box; + box.setTag(app); + box.setChecked(app.isProxyed()); + + entry.text.setTag(box); + + return convertView; + } + }; + + appsLoaded = true; + + } + + /** + * Called an application is check/unchecked + */ + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + final ProxyedApp app = (ProxyedApp) buttonView.getTag(); + if (app != null) { + app.setProxyed(isChecked); + } + + saveAppSettings(this); + + } + + @Override + public void onClick(View v) { + + CheckBox cbox = (CheckBox) v.getTag(); + + final ProxyedApp app = (ProxyedApp) cbox.getTag(); + if (app != null) { + app.setProxyed(!app.isProxyed()); + cbox.setChecked(app.isProxyed()); + } + + saveAppSettings(this); + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.setContentView(R.layout.layout_apps); + + this.dm = ImageLoaderFactory.getImageLoader(this); + + this.overlay = (TextView) View.inflate(this, R.layout.overlay, null); + getWindowManager() + .addView( + overlay, + new WindowManager.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT)); + + mAppManager = this; + + } + + @Override + protected void onResume() { + super.onResume(); + + new Thread() { + + @Override + public void run() { + handler.sendEmptyMessage(MSG_LOAD_START); + + listApps = (ListView) findViewById(R.id.applistview); + + if (!appsLoaded) + loadApps(); + handler.sendEmptyMessage(MSG_LOAD_FINISH); + } + }.start(); + + } + + /* + * (non-Javadoc) + * + * @see android.app.Activity#onStop() + */ + @Override + protected void onStop() { + super.onStop(); + + // Log.d(getClass().getName(),"Exiting Preferences"); + } + + public void saveAppSettings(Context context) { + if (apps == null) + return; + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(this); + + // final SharedPreferences prefs = + // context.getSharedPreferences(PREFS_KEY, 0); + + StringBuilder tordApps = new StringBuilder(); + + for (int i = 0; i < apps.length; i++) { + if (apps[i].isProxyed()) { + tordApps.append(apps[i].getUsername()); + tordApps.append("|"); + } + } + + Editor edit = prefs.edit(); + edit.putString(PREFS_KEY_PROXYED, tordApps.toString()); + edit.commit(); + + } + +} diff --git a/src/com/github/shadowsocks/Exec.java b/src/com/github/shadowsocks/Exec.java new file mode 100755 index 00000000..090723ad --- /dev/null +++ b/src/com/github/shadowsocks/Exec.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.shadowsocks; + +import java.io.FileDescriptor; + +/** + * Utility methods for creating and managing a subprocess. + *

+ * Note: The native methods access a package-private java.io.FileDescriptor + * field to get and set the raw Linux file descriptor. This might break if the + * implementation of java.io.FileDescriptor is changed. + */ + +public class Exec { + static { + System.loadLibrary("exec"); + } + + /** + * Close a given file descriptor. + */ + public static native void close(FileDescriptor fd); + + /** + * Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty + * is used to communicate with the subprocess. + *

+ * Callers are responsible for calling Exec.close() on the returned file + * descriptor. + * + * @param cmd The command to execute + * @param args An array of arguments to the command + * @param envVars An array of strings of the form "VAR=value" to be added to the + * environment of the process + * @param processId A one-element array to which the process ID of the started + * process will be written. + * @return the file descriptor of the started process. + */ + public static native FileDescriptor createSubprocess(String cmd, + String[] args, String[] envVars, int[] processId); + + /** + * Send SIGHUP to a process group. + */ + public static native void hangupProcessGroup(int processId); + + /** + * Causes the calling thread to wait for the process associated with the + * receiver to finish executing. + * + * @return The exit value of the Process being waited on + */ + public static native int waitFor(int processId); +} diff --git a/src/com/github/shadowsocks/ImageLoader.java b/src/com/github/shadowsocks/ImageLoader.java new file mode 100755 index 00000000..6d4378d1 --- /dev/null +++ b/src/com/github/shadowsocks/ImageLoader.java @@ -0,0 +1,215 @@ +package com.github.shadowsocks; + +import android.R; +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.widget.ImageView; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Stack; + +public class ImageLoader { + + // Used to display bitmap in the UI thread + class BitmapDisplayer implements Runnable { + Bitmap bitmap; + ImageView imageView; + + public BitmapDisplayer(Bitmap b, ImageView i) { + bitmap = b; + imageView = i; + } + + @Override + public void run() { + if (bitmap != null) + imageView.setImageBitmap(bitmap); + else + imageView.setImageResource(stub_id); + } + } + + class PhotosLoader extends Thread { + @Override + public void run() { + try { + while (true) { + // thread waits until there are any images to load in the + // queue + if (photosQueue.photosToLoad.size() == 0) + synchronized (photosQueue.photosToLoad) { + photosQueue.photosToLoad.wait(); + } + if (photosQueue.photosToLoad.size() != 0) { + PhotoToLoad photoToLoad; + synchronized (photosQueue.photosToLoad) { + photoToLoad = photosQueue.photosToLoad.pop(); + } + Bitmap bmp = getBitmap(photoToLoad.uid); + cache.put(photoToLoad.uid, bmp); + Object tag = photoToLoad.imageView.getTag(); + if (tag != null && ((Integer) tag) == photoToLoad.uid) { + BitmapDisplayer bd = new BitmapDisplayer(bmp, + photoToLoad.imageView); + Activity a = (Activity) photoToLoad.imageView + .getContext(); + a.runOnUiThread(bd); + } + } + if (Thread.interrupted()) + break; + } + } catch (InterruptedException e) { + // allow thread to exit + } + } + } + + // stores list of photos to download + class PhotosQueue { + private final Stack photosToLoad = new Stack(); + + // removes all instances of this ImageView + public void Clean(ImageView image) { + synchronized (photosToLoad) { + for (int j = 0; j < photosToLoad.size(); ) { + if (photosToLoad.get(j).imageView == image) photosToLoad.remove(j); + else ++j; + } + } + } + } + + // Task for the queue + private class PhotoToLoad { + public int uid; + public ImageView imageView; + + public PhotoToLoad(int u, ImageView i) { + uid = u; + imageView = i; + } + } + + // the simplest in-memory cache implementation. This should be replaced with + // something like SoftReference or BitmapOptions.inPurgeable(since 1.6) + private HashMap cache = new HashMap(); + + private File cacheDir; + + private Context context; + + final int stub_id = R.drawable.sym_def_app_icon; + + PhotosQueue photosQueue = new PhotosQueue(); + + PhotosLoader photoLoaderThread = new PhotosLoader(); + + public ImageLoader(Context c) { + // Make the background thead low priority. This way it will not affect + // the UI performance + photoLoaderThread.setPriority(Thread.NORM_PRIORITY - 1); + + context = c; + + // Find the dir to save cached images + cacheDir = context.getCacheDir(); + + } + + public void clearCache() { + // clear memory cache + cache.clear(); + + // clear SD cache + File[] files = cacheDir.listFiles(); + for (File f : files) + f.delete(); + } + + // decodes image and scales it to reduce memory consumption + private Bitmap decodeFile(File f) { + try { + // decode image size + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeStream(new FileInputStream(f), null, o); + + // Find the correct scale value. It should be the power of 2. + final int REQUIRED_SIZE = 70; + int width_tmp = o.outWidth, height_tmp = o.outHeight; + int scale = 1; + while (true) { + if (width_tmp / 2 < REQUIRED_SIZE + || height_tmp / 2 < REQUIRED_SIZE) + break; + width_tmp /= 2; + height_tmp /= 2; + scale *= 2; + } + + // decode with inSampleSize + BitmapFactory.Options o2 = new BitmapFactory.Options(); + o2.inSampleSize = scale; + return BitmapFactory.decodeStream(new FileInputStream(f), null, o2); + } catch (FileNotFoundException e) { + } + return null; + } + + public void DisplayImage(int uid, Activity activity, ImageView imageView) { + if (cache.containsKey(uid)) + imageView.setImageBitmap(cache.get(uid)); + else { + queuePhoto(uid, activity, imageView); + imageView.setImageResource(stub_id); + } + } + + private Bitmap getBitmap(int uid) { + // I identify images by hashcode. Not a perfect solution, good for the + // demo. + String filename = String.valueOf(uid); + File f = new File(cacheDir, filename); + + // from SD cache + Bitmap b = decodeFile(f); + if (b != null) + return b; + + // from web + try { + BitmapDrawable icon = (BitmapDrawable) Utils.getAppIcon(context, + uid); + return icon.getBitmap(); + } catch (Exception ex) { + return null; + } + } + + private void queuePhoto(int uid, Activity activity, ImageView imageView) { + // This ImageView may be used for other images before. So there may be + // some old tasks in the queue. We need to discard them. + photosQueue.Clean(imageView); + PhotoToLoad p = new PhotoToLoad(uid, imageView); + synchronized (photosQueue.photosToLoad) { + photosQueue.photosToLoad.push(p); + photosQueue.photosToLoad.notifyAll(); + } + + // start thread if it's not started yet + if (photoLoaderThread.getState() == Thread.State.NEW) + photoLoaderThread.start(); + } + + public void stopThread() { + photoLoaderThread.interrupt(); + } + +} diff --git a/src/com/github/shadowsocks/ImageLoaderFactory.java b/src/com/github/shadowsocks/ImageLoaderFactory.java new file mode 100755 index 00000000..b858ad58 --- /dev/null +++ b/src/com/github/shadowsocks/ImageLoaderFactory.java @@ -0,0 +1,50 @@ +// dbartists - Douban artists client for Android +// Copyright (C) 2011 Max Lv +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// +// ___====-_ _-====___ +// _--^^^#####// \\#####^^^--_ +// _-^##########// ( ) \\##########^-_ +// -############// |\^^/| \\############- +// _/############// (@::@) \\############\_ +// /#############(( \\// ))#############\ +// -###############\\ (oo) //###############- +// -#################\\ / VV \ //#################- +// -###################\\/ \//###################- +// _#/|##########/\######( /\ )######/\##########|\#_ +// |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| +// ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' +// ` ` ` ` / | | | | \ ' ' ' ' +// ( | | | | ) +// __\ | | | | /__ +// (vvv(VVV)(VVV)vvv) +// +// HERE BE DRAGONS + +package com.github.shadowsocks; + +import android.content.Context; + +public class ImageLoaderFactory { + + private static ImageLoader il = null; + + public static ImageLoader getImageLoader(Context context) { + if (il == null) { + il = new ImageLoader(context); + } + return il; + } +} diff --git a/src/com/github/shadowsocks/ProxyedApp.java b/src/com/github/shadowsocks/ProxyedApp.java new file mode 100755 index 00000000..7e747c25 --- /dev/null +++ b/src/com/github/shadowsocks/ProxyedApp.java @@ -0,0 +1,96 @@ +package com.github.shadowsocks; + +public class ProxyedApp { + + private boolean enabled; + private int uid; + private String username; + private String procname; + private String name; + + private boolean proxyed = false; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the procname + */ + public String getProcname() { + return procname; + } + + /** + * @return the uid + */ + public int getUid() { + return uid; + } + + /** + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * @return the enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * @return the proxyed + */ + public boolean isProxyed() { + return proxyed; + } + + /** + * @param enabled the enabled to set + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @param procname the procname to set + */ + public void setProcname(String procname) { + this.procname = procname; + } + + /** + * @param proxyed the proxyed to set + */ + public void setProxyed(boolean proxyed) { + this.proxyed = proxyed; + } + + /** + * @param uid the uid to set + */ + public void setUid(int uid) { + this.uid = uid; + } + + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } +} \ No newline at end of file diff --git a/src/com/github/shadowsocks/Shadowsocks.java b/src/com/github/shadowsocks/Shadowsocks.java new file mode 100644 index 00000000..584a565c --- /dev/null +++ b/src/com/github/shadowsocks/Shadowsocks.java @@ -0,0 +1,639 @@ +/* Shadowsocks - GoAgent / WallProxy client App for Android + * Copyright (C) 2012 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * ___====-_ _-====___ + * _--^^^#####// \\#####^^^--_ + * _-^##########// ( ) \\##########^-_ + * -############// |\^^/| \\############- + * _/############// (@::@) \\############\_ + * /#############(( \\// ))#############\ + * -###############\\ (oo) //###############- + * -#################\\ / VV \ //#################- + * -###################\\/ \//###################- + * _#/|##########/\######( /\ )######/\##########|\#_ + * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| + * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' + * ` ` ` ` / | | | | \ ' ' ' ' + * ( | | | | ) + * __\ | | | | /__ + * (vvv(VVV)(VVV)vvv) + * + * HERE BE DRAGONS + * + */ + +package com.github.shadowsocks; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.*; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; +import com.google.analytics.tracking.android.EasyTracker; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Shadowsocks extends PreferenceActivity implements + OnSharedPreferenceChangeListener { + + private static final String TAG = "Shadowsocks"; + + public static final String PREFS_NAME = "Shadowsocks"; + + private static final int MSG_CRASH_RECOVER = 1; + private static final int MSG_INITIAL_FINISH = 2; + + final Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(Shadowsocks.this); + Editor ed = settings.edit(); + switch (msg.what) { + case MSG_CRASH_RECOVER: + Toast.makeText(Shadowsocks.this, R.string.crash_alert, + Toast.LENGTH_LONG).show(); + ed.putBoolean("isRunning", false); + break; + case MSG_INITIAL_FINISH: + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + break; + } + ed.commit(); + super.handleMessage(msg); + } + }; + + private static ProgressDialog mProgressDialog = null; + + private CheckBoxPreference isAutoConnectCheck; + private CheckBoxPreference isGlobalProxyCheck; + private EditTextPreference proxyText; + private EditTextPreference portText; + private EditTextPreference remotePortText; + private EditTextPreference sitekeyText; + private CheckBoxPreference isGFWListCheck; + private CheckBoxPreference isRunningCheck; + private Preference proxyedApps; + private CheckBoxPreference isBypassAppsCheck; + + private void copyAssets(String path) { + + AssetManager assetManager = getAssets(); + String[] files = null; + try { + files = assetManager.list(path); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + if (files != null) { + for (String file : files) { + InputStream in = null; + OutputStream out = null; + try { + in = assetManager.open(file); + out = new FileOutputStream("/data/data/com.github.shadowsocks/" + + file); + copyFile(in, out); + in.close(); + in = null; + out.flush(); + out.close(); + out = null; + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + } + } + } + } + + private void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + + private void crash_recovery() { + + Utils.runRootCommand(Utils.getIptables() + " -t nat -F OUTPUT"); + + Utils.runCommand(ShadowsocksService.BASE + "proxy.sh stop"); + + } + + private void disableAll() { + proxyText.setEnabled(false); + portText.setEnabled(false); + remotePortText.setEnabled(false); + sitekeyText.setEnabled(false); + proxyedApps.setEnabled(false); + isGFWListCheck.setEnabled(false); + isBypassAppsCheck.setEnabled(false); + + isAutoConnectCheck.setEnabled(false); + isGlobalProxyCheck.setEnabled(false); + } + + private void enableAll() { + proxyText.setEnabled(true); + portText.setEnabled(true); + remotePortText.setEnabled(true); + sitekeyText.setEnabled(true); + isGlobalProxyCheck.setEnabled(true); + isGFWListCheck.setEnabled(true); + if (!isGlobalProxyCheck.isChecked()) { + proxyedApps.setEnabled(true); + isBypassAppsCheck.setEnabled(true); + } + + isAutoConnectCheck.setEnabled(true); + } + + private boolean isTextEmpty(String s, String msg) { + if (s == null || s.length() <= 0) { + showAToast(msg); + return true; + } + return false; + } + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.main); + addPreferencesFromResource(R.xml.shadowsocks_preference); + + proxyText = (EditTextPreference) findPreference("proxy"); + portText = (EditTextPreference) findPreference("port"); + remotePortText = (EditTextPreference) findPreference("remotePort"); + sitekeyText = (EditTextPreference) findPreference("sitekey"); + proxyedApps = findPreference("proxyedApps"); + + isRunningCheck = (CheckBoxPreference) findPreference("isRunning"); + isAutoConnectCheck = (CheckBoxPreference) findPreference("isAutoConnect"); + isGlobalProxyCheck = (CheckBoxPreference) findPreference("isGlobalProxy"); + isGFWListCheck = (CheckBoxPreference) findPreference("isGFWList"); + isBypassAppsCheck = (CheckBoxPreference) findPreference("isBypassApps"); + + if (mProgressDialog == null) + mProgressDialog = ProgressDialog.show(this, "", + getString(R.string.initializing), true, true); + + final SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(this); + + new Thread() { + @Override + public void run() { + + Utils.isRoot(); + + String versionName; + try { + versionName = getPackageManager().getPackageInfo( + getPackageName(), 0).versionName; + } catch (NameNotFoundException e) { + versionName = "NONE"; + } + + if (!settings.getBoolean(versionName, false)) { + + Editor edit = settings.edit(); + edit.putBoolean(versionName, true); + edit.commit(); + + reset(); + + } + + handler.sendEmptyMessage(MSG_INITIAL_FINISH); + } + }.start(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(Menu.NONE, Menu.FIRST + 1, 1, getString(R.string.recovery)) + .setIcon(android.R.drawable.ic_menu_delete); + menu.add(Menu.NONE, Menu.FIRST + 2, 2, getString(R.string.about)) + .setIcon(android.R.drawable.ic_menu_info_details); + return true; + + } + + /** + * Called when the activity is closed. + */ + @Override + public void onDestroy() { + SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); + + SharedPreferences.Editor editor = settings.edit(); + editor.putBoolean("isConnected", ShadowsocksService.isServiceStarted()); + editor.commit(); + + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + + super.onDestroy(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { // 按下的如果是BACK,同时没有重复 + try { + finish(); + } catch (Exception ignore) { + // Nothing + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + // 菜单项被选择事件 + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case Menu.FIRST + 1: + recovery(); + break; + case Menu.FIRST + 2: + String versionName = ""; + try { + versionName = getPackageManager().getPackageInfo( + getPackageName(), 0).versionName; + } catch (NameNotFoundException e) { + versionName = ""; + } + showAToast(getString(R.string.about) + " (" + versionName + ")\n\n" + + getString(R.string.copy_rights)); + break; + } + + return true; + } + + @Override + protected void onPause() { + super.onPause(); + + // Unregister the listener whenever a key changes + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(this); + + if (preference.getKey() != null + && preference.getKey().equals("proxyedApps")) { + Intent intent = new Intent(this, AppManager.class); + startActivity(intent); + } else if (preference.getKey() != null + && preference.getKey().equals("isRunning")) { + if (!serviceStart()) { + Editor edit = settings.edit(); + edit.putBoolean("isRunning", false); + edit.commit(); + } + } + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + + @Override + protected void onResume() { + super.onResume(); + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(this); + + if (settings.getBoolean("isSystemProxy", false)) { + isGlobalProxyCheck.setEnabled(false); + isGFWListCheck.setEnabled(false); + proxyedApps.setEnabled(false); + isBypassAppsCheck.setEnabled(false); + } else { + if (settings.getBoolean("isGlobalProxy", false)) { + proxyedApps.setEnabled(false); + isBypassAppsCheck.setEnabled(false); + } else { + proxyedApps.setEnabled(true); + isBypassAppsCheck.setEnabled(true); + } + } + + sitekeyText.setEnabled(true); + + Editor edit = settings.edit(); + + if (ShadowsocksService.isServiceStarted()) { + edit.putBoolean("isRunning", true); + } else { + if (settings.getBoolean("isRunning", false)) { + new Thread() { + @Override + public void run() { + crash_recovery(); + handler.sendEmptyMessage(MSG_CRASH_RECOVER); + } + }.start(); + } + edit.putBoolean("isRunning", false); + } + + edit.commit(); + + if (settings.getBoolean("isRunning", false)) { + isRunningCheck.setChecked(true); + disableAll(); + } else { + isRunningCheck.setChecked(false); + enableAll(); + } + + // Setup the initial values + + if (!settings.getString("sitekey", "").equals("")) + sitekeyText.setSummary(settings.getString("sitekey", "")); + + if (!settings.getString("port", "").equals("")) + portText.setSummary(settings.getString("port", + getString(R.string.port_summary))); + + if (!settings.getString("remotePort", "").equals("")) + portText.setSummary(settings.getString("remotePort", + getString(R.string.port_summary))); + + if (!settings.getString("proxy", "").equals("")) + proxyText.setSummary(settings.getString("proxy", + getString(R.string.proxy_summary))); + + // Set up a listener whenever a key changes + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + // Let's do something a preference value changes + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(this); + + if (key.equals("isConnecting")) { + if (settings.getBoolean("isConnecting", false)) { + Log.d(TAG, "Connecting start"); + if (mProgressDialog == null) + mProgressDialog = ProgressDialog.show(this, "", + getString(R.string.connecting), true, true); + } else { + Log.d(TAG, "Connecting finish"); + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + } + + if (key.equals("isGlobalProxy")) { + if (settings.getBoolean("isGlobalProxy", false)) { + proxyedApps.setEnabled(false); + isBypassAppsCheck.setEnabled(false); + } else { + proxyedApps.setEnabled(true); + isBypassAppsCheck.setEnabled(true); + } + } + + if (key.equals("isRunning")) { + if (settings.getBoolean("isRunning", false)) { + disableAll(); + isRunningCheck.setChecked(true); + } else { + isRunningCheck.setChecked(false); + enableAll(); + } + } + + if (key.equals("remotePort")) + if (settings.getString("remotePort", "").equals("")) + remotePortText.setSummary(getString(R.string.remote_port_summary)); + else + remotePortText.setSummary(settings.getString("remotePort", "")); + else if (key.equals("port")) + if (settings.getString("port", "").equals("")) + portText.setSummary(getString(R.string.port_summary)); + else + portText.setSummary(settings.getString("port", "")); + else if (key.equals("sitekey")) + if (settings.getString("sitekey", "").equals("")) + sitekeyText.setSummary(getString(R.string.sitekey_summary)); + else + sitekeyText.setSummary(settings.getString("sitekey", "")); + else if (key.equals("proxy")) + if (settings.getString("proxy", "").equals("")) { + proxyText.setSummary(getString(R.string.proxy_summary)); + } else { + proxyText.setSummary(settings.getString("proxy", "")); + } + } + + @Override + public void onStart() { + super.onStart(); + EasyTracker.getInstance().activityStart(this); + } + + @Override + public void onStop() { + super.onStop(); + EasyTracker.getInstance().activityStop(this); + } + + public void reset() { + StringBuilder sb = new StringBuilder(); + sb.append(Utils.getIptables()).append(" -t nat -F OUTPUT").append("\n"); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n"); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n"); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`").append("\n"); + sb.append("killall pdnsd").append("\n"); + sb.append("killall redsocks").append("\n"); + sb.append("killall shadowsocks").append("\n"); + Utils.runRootCommand(sb.toString()); + + copyAssets(""); + + Utils.runCommand("chmod 755 /data/data/com.github.shadowsocks/iptables\n" + + "chmod 755 /data/data/com.github.shadowsocks/redsocks\n" + + "chmod 755 /data/data/com.github.shadowsocks/pdnsd\n" + + "chmod 755 /data/data/com.github.shadowsocks/shadowsocks\n" + ); + } + + private void recovery() { + + if (mProgressDialog == null) + mProgressDialog = ProgressDialog.show(this, "", getString(R.string.recovering), + true, true); + + final Handler h = new Handler() { + @Override + public void handleMessage(Message msg) { + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + }; + + try { + stopService(new Intent(this, ShadowsocksService.class)); + } catch (Exception e) { + // Nothing + } + + new Thread() { + @Override + public void run() { + + reset(); + + h.sendEmptyMessage(0); + } + }.start(); + + } + + /** + * Called when connect button is clicked. + */ + public boolean serviceStart() { + + if (ShadowsocksService.isServiceStarted()) { + try { + stopService(new Intent(this, ShadowsocksService.class)); + } catch (Exception e) { + // Nothing + } + return false; + } + + SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(this); + + final String proxy = settings.getString("proxy", ""); + if (isTextEmpty(proxy, getString(R.string.proxy_empty))) + return false; + + if (proxy.contains("proxyofmax.appspot.com")) { + final TextView message = new TextView(this); + message.setPadding(10, 5, 10, 5); + final SpannableString s = new SpannableString( + getText(R.string.default_proxy_alert)); + Linkify.addLinks(s, Linkify.WEB_URLS); + message.setText(s); + message.setMovementMethod(LinkMovementMethod.getInstance()); + + new AlertDialog.Builder(this) + .setTitle(R.string.warning) + .setCancelable(false) + .setIcon(android.R.drawable.ic_dialog_info) + .setNegativeButton(getString(R.string.ok_iknow), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int id) { + dialog.cancel(); + } + }).setView(message).create().show(); + } + + String portText = settings.getString("port", ""); + if (isTextEmpty(portText, getString(R.string.port_empty))) + return false; + try { + int port = Integer.valueOf(portText); + if (port <= 1024) { + this.showAToast(getString(R.string.port_alert)); + return false; + } + } catch (Exception e) { + this.showAToast(getString(R.string.port_alert)); + return false; + } + + try { + Intent it = new Intent(this, ShadowsocksService.class); + startService(it); + } catch (Exception e) { + // Nothing + return false; + } + + return true; + } + + private void showAToast(String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(msg) + .setCancelable(false) + .setNegativeButton(getString(R.string.ok_iknow), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + } + +} diff --git a/src/com/github/shadowsocks/ShadowsocksApplication.java b/src/com/github/shadowsocks/ShadowsocksApplication.java new file mode 100755 index 00000000..b0ac76c6 --- /dev/null +++ b/src/com/github/shadowsocks/ShadowsocksApplication.java @@ -0,0 +1,13 @@ +package com.github.shadowsocks; + +import android.app.Application; +import com.google.analytics.tracking.android.EasyTracker; + +public class ShadowsocksApplication extends Application { + + @Override + public void onCreate() { + EasyTracker.getInstance().setContext(this); + } + +} diff --git a/src/com/github/shadowsocks/ShadowsocksService.java b/src/com/github/shadowsocks/ShadowsocksService.java new file mode 100755 index 00000000..32ae7fbe --- /dev/null +++ b/src/com/github/shadowsocks/ShadowsocksService.java @@ -0,0 +1,575 @@ +/* gaeproxy - GAppProxy / WallProxy client App for Android + * Copyright (C) 2011 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * ___====-_ _-====___ + * _--^^^#####// \\#####^^^--_ + * _-^##########// ( ) \\##########^-_ + * -############// |\^^/| \\############- + * _/############// (@::@) \\############\_ + * /#############(( \\// ))#############\ + * -###############\\ (oo) //###############- + * -#################\\ / VV \ //#################- + * -###################\\/ \//###################- + * _#/|##########/\######( /\ )######/\##########|\#_ + * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| + * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' + * ` ` ` ` / | | | | \ ' ' ' ' + * ( | | | | ) + * __\ | | | | /__ + * (vvv(VVV)(VVV)vvv) + * + * HERE BE DRAGONS + * + */ + +package com.github.shadowsocks; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.util.Log; +import com.google.analytics.tracking.android.EasyTracker; + +import java.io.DataOutputStream; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashSet; + +public class ShadowsocksService extends Service { + + private Notification notification; + private NotificationManager notificationManager; + private PendingIntent pendIntent; + private PowerManager.WakeLock mWakeLock; + + public static final String BASE = "/data/data/com.github.shadowsocks/"; + private static final int MSG_CONNECT_START = 0; + private static final int MSG_CONNECT_FINISH = 1; + private static final int MSG_CONNECT_SUCCESS = 2; + private static final int MSG_CONNECT_FAIL = 3; + private static final int MSG_HOST_CHANGE = 4; + private static final int MSG_STOP_SELF = 5; + + final static String CMD_IPTABLES_RETURN = " -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n"; + + final static String CMD_IPTABLES_REDIRECT_ADD_HTTP = " -t nat -A OUTPUT -p tcp " + + "--dport 80 -j REDIRECT --to 8123\n"; + final static String CMD_IPTABLES_REDIRECT_ADD_HTTPS = " -t nat -A OUTPUT -p tcp " + + "--dport 443 -j REDIRECT --to 8124\n"; + + final static String CMD_IPTABLES_DNAT_ADD_HTTP = " -t nat -A OUTPUT -p tcp " + + "--dport 80 -j DNAT --to-destination 127.0.0.1:8123\n"; + final static String CMD_IPTABLES_DNAT_ADD_HTTPS = " -t nat -A OUTPUT -p tcp " + + "--dport 443 -j DNAT --to-destination 127.0.0.1:8124\n"; + + private static final String TAG = "ShadowsocksService"; + private static final String DEFAULT_HOST = "74.125.128.18"; + private final static int DNS_PORT = 8053; + + private Process httpProcess = null; + private DataOutputStream httpOS = null; + + private String appHost; + private int remotePort; + private int port; + private String sitekey; + + private SharedPreferences settings = null; + + private boolean hasRedirectSupport = true; + private boolean isGlobalProxy = false; + private boolean isGFWList = false; + private boolean isBypassApps = false; + + private ProxyedApp apps[]; + + private static final Class[] mStartForegroundSignature = new Class[]{ + int.class, Notification.class}; + private static final Class[] mStopForegroundSignature = new Class[]{boolean.class}; + private static final Class[] mSetForegroundSignature = new Class[]{boolean.class}; + + private Method mSetForeground; + private Method mStartForeground; + private Method mStopForeground; + + private Object[] mSetForegroundArgs = new Object[1]; + private Object[] mStartForegroundArgs = new Object[2]; + private Object[] mStopForegroundArgs = new Object[1]; + + /* + * This is a hack see + * http://www.mail-archive.com/android-developers@googlegroups + * .com/msg18298.html we are not really able to decide if the service was + * started. So we remember a week reference to it. We set it if we are + * running and clear it if we are stopped. If anything goes wrong, the + * reference will hopefully vanish + */ + private static WeakReference sRunningInstance = null; + + public static boolean isServiceStarted() { + final boolean isServiceStarted; + if (sRunningInstance == null) { + isServiceStarted = false; + } else if (sRunningInstance.get() == null) { + isServiceStarted = false; + sRunningInstance = null; + } else { + isServiceStarted = true; + } + return isServiceStarted; + } + + final Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + Editor ed = settings.edit(); + switch (msg.what) { + case MSG_CONNECT_START: + ed.putBoolean("isConnecting", true); + + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK + | PowerManager.ON_AFTER_RELEASE, "GAEProxy"); + + mWakeLock.acquire(); + + break; + case MSG_CONNECT_FINISH: + ed.putBoolean("isConnecting", false); + + if (mWakeLock != null && mWakeLock.isHeld()) + mWakeLock.release(); + + break; + case MSG_CONNECT_SUCCESS: + ed.putBoolean("isRunning", true); + break; + case MSG_CONNECT_FAIL: + ed.putBoolean("isRunning", false); + break; + case MSG_HOST_CHANGE: + ed.putString("appHost", appHost); + break; + case MSG_STOP_SELF: + stopSelf(); + break; + } + ed.commit(); + super.handleMessage(msg); + } + }; + + public void startShadowsocksDaemon() { + final String cmd = String.format(BASE + + "shadowsocks \"%s\" \"%d\" \"%d\" \"%s\"", + appHost, remotePort, port, sitekey); + Utils.runRootCommand(cmd); + } + + public void startDnsDaemon() { + final String cmd = BASE + "pdnsd -c " + BASE + "pdnsd.conf"; + Utils.runRootCommand(cmd); + } + + private String getVersionName() { + String version; + try { + PackageInfo pi = getPackageManager().getPackageInfo( + getPackageName(), 0); + version = pi.versionName; + } catch (PackageManager.NameNotFoundException e) { + version = "Package name not found"; + } + return version; + } + + public void handleCommand(Intent intent) { + + if (intent == null) { + stopSelf(); + return; + } + + appHost = settings.getString("proxy", "127.0.0.1"); + sitekey = settings.getString("sitekey", "default"); + try { + remotePort = Integer.valueOf(settings.getString("remotePort", "1984")); + } catch (NumberFormatException ex) { + remotePort = 1984; + } + try { + port = Integer.valueOf(settings.getString("port", "1984")); + } catch (NumberFormatException ex) { + port = 1984; + } + + isGlobalProxy = settings.getBoolean("isGlobalProxy", false); + isGFWList = settings.getBoolean("isGFWList", false); + isBypassApps = settings.getBoolean("isBypassApps", false); + + new Thread(new Runnable() { + @Override + public void run() { + + handler.sendEmptyMessage(MSG_CONNECT_START); + + Log.d(TAG, "IPTABLES: " + Utils.getIptables()); + + // Test for Redirect Support + hasRedirectSupport = Utils.getHasRedirectSupport(); + + if (handleConnection()) { + // Connection and forward successful + notifyAlert(getString(R.string.forward_success), + getString(R.string.service_running)); + + handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 500); + + + } else { + // Connection or forward unsuccessful + notifyAlert(getString(R.string.forward_fail), + getString(R.string.service_failed)); + + stopSelf(); + handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500); + } + + handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 500); + + } + }).start(); + markServiceStarted(); + } + + /** + * Called when the activity is first created. + */ + public boolean handleConnection() { + + startShadowsocksDaemon(); + startDnsDaemon(); + setupIptables(); + + return true; + } + + private void initSoundVibrateLights(Notification notification) { + notification.sound = null; + notification.defaults |= Notification.DEFAULT_LIGHTS; + } + + void invokeMethod(Method method, Object[] args) { + try { + method.invoke(this, mStartForegroundArgs); + } catch (InvocationTargetException e) { + // Should not happen. + Log.w(TAG, "Unable to invoke method", e); + } catch (IllegalAccessException e) { + // Should not happen. + Log.w(TAG, "Unable to invoke method", e); + } + } + + private void markServiceStarted() { + sRunningInstance = new WeakReference(this); + } + + private void markServiceStopped() { + sRunningInstance = null; + } + + private void notifyAlert(String title, String info) { + notification.icon = R.drawable.ic_stat_gaeproxy; + notification.tickerText = title; + notification.flags = Notification.FLAG_ONGOING_EVENT; + initSoundVibrateLights(notification); + // notification.defaults = Notification.DEFAULT_SOUND; + notification.setLatestEventInfo(this, getString(R.string.app_name), + info, pendIntent); + startForegroundCompat(1, notification); + } + + private void notifyAlert(String title, String info, int flags) { + notification.icon = R.drawable.ic_stat_gaeproxy; + notification.tickerText = title; + notification.flags = flags; + initSoundVibrateLights(notification); + notification.setLatestEventInfo(this, getString(R.string.app_name), + info, pendIntent); + notificationManager.notify(0, notification); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + EasyTracker.getTracker().trackEvent("service", "start", + getVersionName(), 0L); + + settings = PreferenceManager.getDefaultSharedPreferences(this); + notificationManager = (NotificationManager) this + .getSystemService(NOTIFICATION_SERVICE); + + Intent intent = new Intent(this, Shadowsocks.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + pendIntent = PendingIntent.getActivity(this, 0, intent, 0); + notification = new Notification(); + + try { + mStartForeground = getClass().getMethod("startForeground", + mStartForegroundSignature); + mStopForeground = getClass().getMethod("stopForeground", + mStopForegroundSignature); + } catch (NoSuchMethodException e) { + // Running on an older platform. + mStartForeground = mStopForeground = null; + } + + try { + mSetForeground = getClass().getMethod("setForeground", + mSetForegroundSignature); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "OS doesn't have Service.startForeground OR Service.setForeground!"); + } + } + + /** + * Called when the activity is closed. + */ + @Override + public void onDestroy() { + + EasyTracker.getTracker().trackEvent("service", "stop", + getVersionName(), 0L); + + stopForegroundCompat(1); + + notifyAlert(getString(R.string.forward_stop), + getString(R.string.service_stopped), + Notification.FLAG_AUTO_CANCEL); + + try { + if (httpOS != null) { + httpOS.close(); + httpOS = null; + } + if (httpProcess != null) { + httpProcess.destroy(); + httpProcess = null; + } + } catch (Exception e) { + Log.e(TAG, "HTTP Server close unexpected"); + } + + new Thread() { + @Override + public void run() { + + // Make sure the connection is closed, important here + onDisconnect(); + + } + }.start(); + + Editor ed = settings.edit(); + ed.putBoolean("isRunning", false); + ed.putBoolean("isConnecting", false); + ed.commit(); + + try { + notificationManager.cancel(0); + } catch (Exception ignore) { + // Nothing + } + + super.onDestroy(); + + markServiceStopped(); + } + + private void onDisconnect() { + Utils.runRootCommand(Utils.getIptables() + " -t nat -F OUTPUT"); + StringBuilder sb = new StringBuilder(); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n"); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n"); + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`").append("\n"); + Utils.runRootCommand(sb.toString()); + } + + // This is the old onStart method that will be called on the pre-2.0 + // platform. On 2.0 or later we override onStartCommand() so this + // method will not be called. + @Override + public void onStart(Intent intent, int startId) { + + handleCommand(intent); + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handleCommand(intent); + // We want this service to continue running until it is explicitly + // stopped, so return sticky. + return START_STICKY; + } + + private boolean setupIptables() { + + StringBuilder init_sb = new StringBuilder(); + + StringBuilder http_sb = new StringBuilder(); + + StringBuilder https_sb = new StringBuilder(); + + init_sb.append(Utils.getIptables()).append(" -t nat -F OUTPUT\n"); + + if (hasRedirectSupport) { + init_sb.append(Utils.getIptables()).append(" -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to ").append(DNS_PORT).append("\n"); + } else { + init_sb.append(Utils.getIptables()).append(" -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:").append(DNS_PORT).append("\n"); + } + + String cmd_bypass = Utils.getIptables() + CMD_IPTABLES_RETURN; + + init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "--dport " + remotePort)); + + init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + + getApplicationInfo().uid)); + + if (isGFWList) { + String[] chn_list = getResources().getStringArray(R.array.chn_list); + + for (String item : chn_list) { + init_sb.append(cmd_bypass.replace("0.0.0.0", item)); + } + } + if (isGlobalProxy || isBypassApps) { + http_sb.append(hasRedirectSupport ? Utils.getIptables() + + CMD_IPTABLES_REDIRECT_ADD_HTTP : Utils.getIptables() + + CMD_IPTABLES_DNAT_ADD_HTTP); + https_sb.append(hasRedirectSupport ? Utils.getIptables() + + CMD_IPTABLES_REDIRECT_ADD_HTTPS : Utils.getIptables() + + CMD_IPTABLES_DNAT_ADD_HTTPS); + } + if (!isGlobalProxy) { + // for proxy specified apps + if (apps == null || apps.length <= 0) + apps = AppManager.getProxyedApps(this); + + HashSet uidSet = new HashSet(); + for (ProxyedApp app : apps) { + if (app.isProxyed()) { + uidSet.add(app.getUid()); + } + } + for (int uid : uidSet) { + if (!isBypassApps) { + http_sb.append((hasRedirectSupport ? Utils.getIptables() + + CMD_IPTABLES_REDIRECT_ADD_HTTP : Utils.getIptables() + + CMD_IPTABLES_DNAT_ADD_HTTP).replace("-t nat", + "-t nat -m owner --uid-owner " + uid)); + https_sb.append((hasRedirectSupport ? Utils.getIptables() + + CMD_IPTABLES_REDIRECT_ADD_HTTPS : Utils.getIptables() + + CMD_IPTABLES_DNAT_ADD_HTTPS).replace("-t nat", + "-t nat -m owner --uid-owner " + uid)); + } else { + init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid)); + } + } + } + + String init_rules = init_sb.toString(); + Utils.runRootCommand(init_rules, 30 * 1000); + + String redt_rules = http_sb.toString(); + + redt_rules += https_sb.toString(); + + Utils.runRootCommand(redt_rules); + + return true; + + } + + /** + * This is a wrapper around the new startForeground method, using the older + * APIs if it is not available. + */ + void startForegroundCompat(int id, Notification notification) { + // If we have the new startForeground API, then use it. + if (mStartForeground != null) { + mStartForegroundArgs[0] = id; + mStartForegroundArgs[1] = notification; + invokeMethod(mStartForeground, mStartForegroundArgs); + return; + } + + // Fall back on the old API. + mSetForegroundArgs[0] = Boolean.TRUE; + invokeMethod(mSetForeground, mSetForegroundArgs); + notificationManager.notify(id, notification); + } + + /** + * This is a wrapper around the new stopForeground method, using the older + * APIs if it is not available. + */ + void stopForegroundCompat(int id) { + // If we have the new stopForeground API, then use it. + if (mStopForeground != null) { + mStopForegroundArgs[0] = Boolean.TRUE; + try { + mStopForeground.invoke(this, mStopForegroundArgs); + } catch (InvocationTargetException e) { + // Should not happen. + Log.w(TAG, "Unable to invoke stopForeground", e); + } catch (IllegalAccessException e) { + // Should not happen. + Log.w(TAG, "Unable to invoke stopForeground", e); + } + return; + } + + // Fall back on the old API. Note to cancel BEFORE changing the + // foreground state, since we could be killed at that point. + notificationManager.cancel(id); + mSetForegroundArgs[0] = Boolean.FALSE; + invokeMethod(mSetForeground, mSetForegroundArgs); + } + +} diff --git a/src/com/github/shadowsocks/Utils.java b/src/com/github/shadowsocks/Utils.java new file mode 100755 index 00000000..5470ce6a --- /dev/null +++ b/src/com/github/shadowsocks/Utils.java @@ -0,0 +1,427 @@ +package com.github.shadowsocks; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.util.Log; + +import java.io.*; +import java.util.ArrayList; + +public class Utils { + + /** + * Internal thread used to execute scripts (as root or not). + */ + private static final class ScriptRunner extends Thread { + private final File file; + private final String script; + private final StringBuilder res; + private final boolean asroot; + public int exitcode = -1; + // private Process exec; + private int mProcId; + private FileDescriptor mTermFd; + + /** + * Creates a new script runner. + * + * @param file temporary script file + * @param script script to run + * @param res response output + * @param asroot if true, executes the script as root + */ + public ScriptRunner(File file, String script, StringBuilder res, + boolean asroot) { + this.file = file; + this.script = script; + this.res = res; + this.asroot = asroot; + } + + private int createSubprocess(int[] processId, String cmd) { + ArrayList argList = parse(cmd); + String arg0 = argList.get(0); + String[] args = argList.toArray(new String[1]); + + mTermFd = Exec.createSubprocess(arg0, args, null, processId); + return processId[0]; + } + + /** + * Destroy this script runner + */ + @Override + public synchronized void destroy() { + try { + Exec.hangupProcessGroup(mProcId); + Exec.close(mTermFd); + } catch (NoClassDefFoundError ignore) { + // Nothing + } + } + + private ArrayList parse(String cmd) { + final int PLAIN = 0; + final int WHITESPACE = 1; + final int INQUOTE = 2; + int state = WHITESPACE; + ArrayList result = new ArrayList(); + int cmdLen = cmd.length(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < cmdLen; i++) { + char c = cmd.charAt(i); + if (state == PLAIN) { + if (Character.isWhitespace(c)) { + result.add(builder.toString()); + builder.delete(0, builder.length()); + state = WHITESPACE; + } else if (c == '"') { + state = INQUOTE; + } else { + builder.append(c); + } + } else if (state == WHITESPACE) { + if (Character.isWhitespace(c)) { + // do nothing + } else if (c == '"') { + state = INQUOTE; + } else { + state = PLAIN; + builder.append(c); + } + } else if (state == INQUOTE) { + if (c == '\\') { + if (i + 1 < cmdLen) { + i += 1; + builder.append(cmd.charAt(i)); + } + } else if (c == '"') { + state = PLAIN; + } else { + builder.append(c); + } + } + } + if (builder.length() > 0) { + result.add(builder.toString()); + } + return result; + } + + @Override + public void run() { + try { + new File(DEFOUT_FILE).createNewFile(); + file.createNewFile(); + final String abspath = file.getAbsolutePath(); + + // TODO: Rewrite this line + // make sure we have execution permission on the script file + // Runtime.getRuntime().exec("chmod 755 " + abspath).waitFor(); + + // Write the script to be executed + final OutputStreamWriter out = new OutputStreamWriter( + new FileOutputStream(file)); + out.write("#!/system/bin/sh\n"); + out.write(script); + if (!script.endsWith("\n")) + out.write("\n"); + out.write("exit\n"); + out.flush(); + out.close(); + + if (this.asroot) { + // Create the "su" request to run the script + // exec = Runtime.getRuntime().exec( + // root_shell + " -c " + abspath); + + int pid[] = new int[1]; + mProcId = createSubprocess(pid, root_shell + " -c " + + abspath); + } else { + // Create the "sh" request to run the script + // exec = Runtime.getRuntime().exec(getShell() + " " + + // abspath); + + int pid[] = new int[1]; + mProcId = createSubprocess(pid, getShell() + " " + abspath); + } + + final InputStream stdout = new FileInputStream(DEFOUT_FILE); + final byte buf[] = new byte[8192]; + int read = 0; + + exitcode = Exec.waitFor(mProcId); + + // Read stdout + while (stdout.available() > 0) { + read = stdout.read(buf); + if (res != null) + res.append(new String(buf, 0, read)); + } + + } catch (Exception ex) { + if (res != null) + res.append("\n" + ex); + } finally { + destroy(); + } + } + } + + public final static String TAG = "Shadowsocks"; + + public final static String DEFAULT_SHELL = "/system/bin/sh"; + public final static String DEFAULT_ROOT = "/system/bin/su"; + + public final static String ALTERNATIVE_ROOT = "/system/xbin/su"; + public final static String DEFAULT_IPTABLES = "/data/data/com.github.shadowsocks/iptables"; + public final static String ALTERNATIVE_IPTABLES = "/system/bin/iptables"; + public final static String SCRIPT_FILE = "/data/data/com.github.shadowsocks/script"; + public final static String DEFOUT_FILE = "/data/data/com.github.shadowsocks/defout"; + + public final static int TIME_OUT = -99; + private static boolean initialized = false; + private static int hasRedirectSupport = -1; + + private static int isRoot = -1; + private static String shell = null; + private static String root_shell = null; + private static String iptables = null; + + private static String data_path = null; + + private static void checkIptables() { + + if (!isRoot()) { + iptables = DEFAULT_IPTABLES; + return; + } + + // Check iptables binary + iptables = DEFAULT_IPTABLES; + + String lines = null; + + boolean compatible = false; + boolean version = false; + + StringBuilder sb = new StringBuilder(); + String command = iptables + " --version\n" + iptables + + " -L -t nat -n\n" + "exit\n"; + + int exitcode = runScript(command, sb, 10 * 1000, true); + + if (exitcode == TIME_OUT) + return; + + lines = sb.toString(); + + if (lines.contains("OUTPUT")) { + compatible = true; + } + if (lines.contains("v1.4.")) { + version = true; + } + + if (!compatible || !version) { + iptables = ALTERNATIVE_IPTABLES; + if (!new File(iptables).exists()) + iptables = "iptables"; + } + + } + + public static Drawable getAppIcon(Context c, int uid) { + PackageManager pm = c.getPackageManager(); + Drawable appIcon = c.getResources().getDrawable( + android.R.drawable.sym_def_app_icon); + String[] packages = pm.getPackagesForUid(uid); + + if (packages != null) { + if (packages.length == 1) { + try { + ApplicationInfo appInfo = pm.getApplicationInfo( + packages[0], 0); + appIcon = pm.getApplicationIcon(appInfo); + } catch (NameNotFoundException e) { + Log.e(c.getPackageName(), + "No package found matching with the uid " + uid); + } + } + } else { + Log.e(c.getPackageName(), "Package not found for uid " + uid); + } + + return appIcon; + } + + public static String getDataPath(Context ctx) { + + if (data_path == null) { + + if (Environment.MEDIA_MOUNTED.equals(Environment + .getExternalStorageState())) { + data_path = Environment.getExternalStorageDirectory() + .getAbsolutePath(); + } else { + data_path = ctx.getFilesDir().getAbsolutePath(); + } + + Log.d(TAG, "Python Data Path: " + data_path); + } + + return data_path; + } + + public static boolean getHasRedirectSupport() { + if (hasRedirectSupport == -1) + initHasRedirectSupported(); + return hasRedirectSupport == 1 ? true : false; + } + + public static String getIptables() { + if (iptables == null) + checkIptables(); + return iptables; + } + + private static String getShell() { + if (shell == null) { + shell = DEFAULT_SHELL; + if (!new File(shell).exists()) + shell = "sh"; + } + return shell; + } + + public static void initHasRedirectSupported() { + + if (!Utils.isRoot()) + return; + + StringBuilder sb = new StringBuilder(); + String command = Utils.getIptables() + + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"; + + int exitcode = runScript(command, sb, 10 * 1000, true); + + String lines = sb.toString(); + + hasRedirectSupport = 1; + + // flush the check command + Utils.runRootCommand(command.replace("-A", "-D")); + + if (exitcode == TIME_OUT) + return; + + if (lines.contains("No chain/target/match")) { + hasRedirectSupport = 0; + } + } + + public static boolean isInitialized() { + if (initialized) + return true; + else { + initialized = true; + return false; + } + + } + + public static boolean isRoot() { + + if (isRoot != -1) + return isRoot == 1 ? true : false; + + // switch between binaries + if (new File(DEFAULT_ROOT).exists()) { + root_shell = DEFAULT_ROOT; + } else if (new File(ALTERNATIVE_ROOT).exists()) { + root_shell = ALTERNATIVE_ROOT; + } else { + root_shell = "su"; + } + + String lines = null; + + StringBuilder sb = new StringBuilder(); + String command = "ls /\n" + "exit\n"; + + int exitcode = runScript(command, sb, 10 * 1000, true); + + if (exitcode == TIME_OUT) { + return false; + } + + lines = sb.toString(); + + if (lines.contains("system")) { + isRoot = 1; + } + + return isRoot == 1 ? true : false; + } + + public static boolean runCommand(String command) { + + return runCommand(command, 10 * 1000); + + } + + public static boolean runCommand(String command, int timeout) { + + Log.d(TAG, command); + + runScript(command, null, timeout, false); + + return true; + } + + public static boolean runRootCommand(String command) { + + return runRootCommand(command, 10 * 1000); + + } + + public static boolean runRootCommand(String command, int timeout) { + + if (!isRoot()) + return false; + + Log.d(TAG, command); + + runScript(command, null, timeout, true); + + return true; + } + + private synchronized static int runScript(String script, StringBuilder res, + long timeout, boolean asroot) { + final File file = new File(SCRIPT_FILE); + final ScriptRunner runner = new ScriptRunner(file, script, res, asroot); + runner.start(); + try { + if (timeout > 0) { + runner.join(timeout); + } else { + runner.join(); + } + if (runner.isAlive()) { + // Timed-out + runner.destroy(); + runner.join(1000); + return TIME_OUT; + } + } catch (InterruptedException ex) { + return TIME_OUT; + } + return runner.exitcode; + } +} diff --git a/travis.keystore b/travis.keystore new file mode 100755 index 00000000..fb6d8520 Binary files /dev/null and b/travis.keystore differ