├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── release ├── run └── src ├── main ├── java │ └── net │ │ └── intelie │ │ └── slowproxy │ │ ├── CyclicCounter.java │ │ ├── DelayedOutputStream.java │ │ ├── HostDefinition.java │ │ ├── Main.java │ │ ├── Options.java │ │ ├── ServerTask.java │ │ ├── SpeedDefinition.java │ │ ├── StatusTask.java │ │ ├── Throttler.java │ │ ├── TransferTask.java │ │ └── Util.java └── resources │ └── usage.txt ├── misc └── stub.sh └── test └── java └── net └── intelie └── slowproxy ├── DelayedOutputStreamTest.java ├── OptionsTest.java └── SpeedDefinitionTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.ipr 3 | *.iws 4 | *.log 5 | *.pyc 6 | *.versionsBackup 7 | .ideas 8 | .idea 9 | .DS_Store 10 | /target 11 | /*/target 12 | .jhw-cache 13 | ldapdata 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Intelie. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | 3. Neither the name of Intelie nor the names of its contributors may 14 | be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY Intelie ''AS IS'' AND ANY EXPRESS OR IMPLIED 18 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 19 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those of the 28 | authors and should not be interpreted as representing official policies, either expressed 29 | or implied, of Intelie. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Run this in the terminal (downloads the latest binary to the current directory): 4 | 5 | LOCATION=`curl -i https://github.com/intelie/slowproxy/releases/latest | perl -n -e '/^Location: \r*([^\r]*)\r*$/ && print "$1"'` && 6 | curl -L ${LOCATION/\/tag\///download/}/slowproxy > slowproxy && 7 | chmod a+x slowproxy 8 | 9 | Or this, if you want to install it system-wide: 10 | 11 | LOCATION=`curl -i https://github.com/intelie/slowproxy/releases/latest | perl -n -e '/^Location: \r*([^\r]*)\r*$/ && print "$1"'` && 12 | curl -L ${LOCATION/\/tag\///download/}/slowproxy > slowproxy && 13 | chmod a+x slowproxy && 14 | sudo mv slowproxy /usr/local/bin 15 | 16 | ## Usage 17 | 18 | $ ./slowproxy @56kbps 1234 somehost:5678 19 | 20 | Proxies requests from localhost:1234 to somehost:5678, limited to 56kbps 21 | 22 | $ ./slowproxy @2mbps/56kbps 1234 somehost:5678 23 | 24 | Proxies requests from localhost:1234 to somehost:5678, limited to 2mbps download and 56kbps upload 25 | 26 | $ ./slowproxy @2mbps:200ms 1234 somehost:5678 27 | 28 | Proxies requests from localhost:1234 to somehost:5678, limited to 2mbps download with a fixed 200ms latency 29 | 30 | 31 | ## TODO 32 | 33 | * Better shell 34 | * Simulate network stall 35 | * Simulate variable latency 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | net.intelie.slowproxy 8 | slowproxy 9 | 0.5-SNAPSHOT 10 | 11 | 12 | 13 | 14 | junit 15 | junit 16 | 4.11 17 | test 18 | 19 | 20 | org.easytesting 21 | fest-assert 22 | 1.4 23 | test 24 | 25 | 26 | 27 | 28 | 29 | 30 | src/main/resources 31 | true 32 | 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-compiler-plugin 39 | 2.3.2 40 | 41 | 1.6 42 | 1.6 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-jar-plugin 48 | 2.3.2 49 | 50 | 51 | 52 | 53 | true 54 | net.intelie.slowproxy.Main 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-shade-plugin 62 | 1.5 63 | 64 | 65 | package 66 | 67 | shade 68 | 69 | 70 | 71 | 72 | 73 | org.codehaus.mojo 74 | exec-maven-plugin 75 | 1.2.1 76 | 77 | 78 | 79 | exec 80 | 81 | 82 | 83 | 84 | net.intelie.slowproxy.Main 85 | 86 | 87 | 88 | maven-antrun-plugin 89 | 1.7 90 | 91 | 92 | merge 93 | package 94 | 95 | 96 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | run 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ $# -lt 2 ]; then 5 | echo 'release ' 6 | echo 'release 1.0 1.1' 7 | exit 0 8 | fi 9 | 10 | mvn clean package 11 | mvn versions:set -DnewVersion=$1 12 | git commit -am "release version $1" 13 | git tag v$1 14 | 15 | mvn clean package 16 | 17 | mvn versions:set -DnewVersion=$2-SNAPSHOT 18 | git commit -am "next version $2-SNAPSHOT" 19 | git push 20 | git push --tags 21 | 22 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | args="$@" 2 | exec mvn -q compile exec:java -Dexec.args="$args" 3 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/CyclicCounter.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | public class CyclicCounter { 6 | private final int maxVal; 7 | private final AtomicInteger ai = new AtomicInteger(0); 8 | 9 | public CyclicCounter(int maxVal) { 10 | this.maxVal = maxVal; 11 | } 12 | 13 | public void set(int newVal) { 14 | ai.set(newVal); 15 | } 16 | 17 | public int get() { 18 | return ai.get(); 19 | } 20 | 21 | public int getAndIncrement() { 22 | int curVal, newVal; 23 | do { 24 | curVal = this.ai.get(); 25 | newVal = (curVal + 1) % this.maxVal; 26 | } while (!this.ai.compareAndSet(curVal, newVal)); 27 | return curVal; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/DelayedOutputStream.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | import java.util.concurrent.Semaphore; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class DelayedOutputStream extends OutputStream { 11 | private final OutputStream inner; 12 | private final long delay; 13 | private final Semaphore full, empty; 14 | private final byte[] buffer; 15 | private final ScheduledExecutorService executor; 16 | 17 | private volatile int start = 0, end = 0; 18 | private volatile IOException exception; 19 | 20 | public DelayedOutputStream(OutputStream inner, long delay, int bufferSize) { 21 | this(inner, delay, bufferSize, Executors.newSingleThreadScheduledExecutor()); 22 | } 23 | 24 | private DelayedOutputStream(OutputStream inner, long delay, int bufferSize, ScheduledExecutorService executor) { 25 | this.inner = inner; 26 | this.executor = executor; 27 | this.buffer = new byte[bufferSize]; 28 | this.delay = delay; 29 | this.full = new Semaphore(0); 30 | this.empty = new Semaphore(bufferSize); 31 | } 32 | 33 | @Override 34 | public void write(int i) throws IOException { 35 | write(new byte[]{(byte) i}); 36 | } 37 | 38 | @Override 39 | public void write(byte[] b, int off, final int len) throws IOException { 40 | if (exception != null) 41 | throw exception; 42 | 43 | empty.acquireUninterruptibly(len); 44 | if (end + len < buffer.length) { 45 | System.arraycopy(b, off, buffer, end, len); 46 | 47 | } else { 48 | int first = buffer.length - end; 49 | System.arraycopy(b, off, buffer, end, first); 50 | System.arraycopy(b, off + first, buffer, 0, len - first); 51 | } 52 | end = (end + len) % buffer.length; 53 | 54 | executor.schedule(new Runnable() { 55 | @Override 56 | public void run() { 57 | if (exception != null) 58 | return; 59 | full.acquireUninterruptibly(len); 60 | try { 61 | if (start + len < buffer.length) { 62 | inner.write(buffer, start, len); 63 | } else { 64 | int first = buffer.length - start; 65 | inner.write(buffer, start, first); 66 | inner.write(buffer, 0, len - first); 67 | } 68 | 69 | start = (start + len) % buffer.length; 70 | } catch (IOException e) { 71 | exception = e; 72 | } finally { 73 | empty.release(len); 74 | } 75 | } 76 | }, delay, TimeUnit.MILLISECONDS); 77 | 78 | full.release(len); 79 | } 80 | 81 | @Override 82 | public void flush() throws IOException { 83 | executor.shutdown(); 84 | try { 85 | executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); 86 | } catch (InterruptedException e) { 87 | Thread.currentThread().interrupt(); 88 | } 89 | inner.flush(); 90 | } 91 | 92 | @Override 93 | public void close() throws IOException { 94 | flush(); 95 | inner.close(); 96 | if (exception != null) 97 | throw exception; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/HostDefinition.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | 8 | public class HostDefinition { 9 | private final String host; 10 | private final int port; 11 | 12 | public HostDefinition(String host, int port) { 13 | this.host = host; 14 | this.port = port; 15 | } 16 | 17 | public String host() { 18 | return host; 19 | } 20 | 21 | public int port() { 22 | return port; 23 | } 24 | 25 | public static HostDefinition parse(String s) { 26 | String[] strings = s.split(":", 2); 27 | return new HostDefinition(strings[0], Integer.parseInt(strings[1])); 28 | } 29 | 30 | public static HostDefinition parseWithOptionalHost(String s) { 31 | String[] strings = s.split(":", 2); 32 | if (strings.length == 1) 33 | return new HostDefinition(null, Integer.parseInt(strings[0])); 34 | else 35 | return new HostDefinition(strings[0], Integer.parseInt(strings[1])); 36 | } 37 | 38 | public Socket newSocket() throws IOException { 39 | return new Socket(host, port); 40 | } 41 | 42 | public ServerSocket newServerSocket() throws IOException { 43 | if (host != null) { 44 | InetAddress address = InetAddress.getByName(host); 45 | return new ServerSocket(port, 50, address); 46 | } else { 47 | return new ServerSocket(port); 48 | } 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return (host != null ? host : "") + ":" + port; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/Main.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | 11 | public class Main { 12 | public static void main(String[] args) throws Exception { 13 | if (args.length == 0) 14 | printUsageAndExit(); 15 | final Options options; 16 | try { 17 | options = Options.parse(args); 18 | } catch (Exception e) { 19 | System.out.println(e.getClass().getName() + ": " + e.getMessage()); 20 | printUsageAndExit(); 21 | return; 22 | } 23 | 24 | ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(1); 25 | ExecutorService executor = Executors.newCachedThreadPool(); 26 | CyclicCounter currentHost = new CyclicCounter(options.remote().size()); 27 | 28 | executor.submit(new ServerTask(options, scheduled, executor, currentHost)); 29 | 30 | String line; 31 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 32 | while ((line = reader.readLine()) != null && !"q".equals(line)) { 33 | int nextHost = setNextHost(line, options.remote().size(), currentHost.get()); 34 | if (nextHost < 0) { 35 | System.out.println("Invalid host number: " + line); 36 | } else { 37 | System.out.println("Using host #" + nextHost + ": " + options.remote().get(nextHost)); 38 | currentHost.set(nextHost); 39 | } 40 | } 41 | if ("q".equals(line)) 42 | System.exit(0); 43 | else 44 | Thread.sleep(Long.MAX_VALUE); 45 | } 46 | 47 | private static void printUsageAndExit() throws IOException { 48 | InputStream input = Main.class.getResourceAsStream("/usage.txt"); 49 | byte[] buffer = new byte[1024]; // Adjust if you want 50 | int bytesRead; 51 | while ((bytesRead = input.read(buffer)) != -1) { 52 | System.out.write(buffer, 0, bytesRead); 53 | } 54 | System.exit(1); 55 | } 56 | 57 | private static int setNextHost(String line, int hostCount, int currentHost) { 58 | int nextHost = (currentHost + 1) % hostCount; 59 | if (!line.isEmpty()) { 60 | try { 61 | nextHost = Integer.parseInt(line); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | if (nextHost < 0 || nextHost >= hostCount) { 67 | return -1; 68 | } else { 69 | return nextHost; 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/Options.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class Options { 9 | private final boolean balance; 10 | private final SpeedDefinition speed; 11 | private final HostDefinition local; 12 | private final List remote; 13 | 14 | public Options(boolean balance, SpeedDefinition speed, HostDefinition local, List remote) { 15 | this.balance = balance; 16 | this.speed = speed; 17 | this.local = local; 18 | this.remote = remote; 19 | } 20 | 21 | public boolean balance() { 22 | return balance; 23 | } 24 | 25 | public SpeedDefinition speed() { 26 | return speed; 27 | } 28 | 29 | public HostDefinition local() { 30 | return local; 31 | } 32 | 33 | public List remote() { 34 | return remote; 35 | } 36 | 37 | public static Options parse(String... args) { 38 | List remoteHosts = new ArrayList(); 39 | ArrayDeque deque = new ArrayDeque(Arrays.asList(args)); 40 | 41 | boolean balance = "balance".equals(deque.peekFirst()); 42 | if (balance) deque.poll(); 43 | 44 | SpeedDefinition speedDefinition = deque.peekFirst().charAt(0) == '@' ? SpeedDefinition.parse(deque.pollFirst().substring(1)) : SpeedDefinition.NO_LIMIT; 45 | 46 | HostDefinition localHost = HostDefinition.parseWithOptionalHost(deque.pollFirst()); 47 | while (!deque.isEmpty()) 48 | remoteHosts.add(HostDefinition.parse(deque.pollFirst())); 49 | return new Options(balance, speedDefinition, localHost, remoteHosts); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/ServerTask.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.OutputStream; 4 | import java.net.ServerSocket; 5 | import java.net.Socket; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Future; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | class ServerTask implements Runnable { 13 | private final AtomicLong totalUpload = new AtomicLong(0); 14 | private final AtomicLong totalDownload = new AtomicLong(0); 15 | private final AtomicLong connected = new AtomicLong(0); 16 | 17 | private final Options options; 18 | private final ScheduledExecutorService scheduled; 19 | private final ExecutorService executor; 20 | private final CyclicCounter currentHost; 21 | 22 | public ServerTask(Options options, ScheduledExecutorService scheduled, ExecutorService executor, CyclicCounter currentHost) { 23 | this.options = options; 24 | this.scheduled = scheduled; 25 | this.executor = executor; 26 | this.currentHost = currentHost; 27 | } 28 | 29 | @Override 30 | public void run() { 31 | try { 32 | ServerSocket server = options.local().newServerSocket(); 33 | scheduled.scheduleAtFixedRate(new StatusTask(totalUpload, totalDownload, connected), 1, 1, TimeUnit.SECONDS); 34 | 35 | System.out.println("Listening at " + server.getLocalSocketAddress()); 36 | 37 | final SpeedDefinition speed = options.speed(); 38 | 39 | if (speed.maxUploadBytes() >= 0) { 40 | System.out.print("The upload speed is limited to " + SpeedDefinition.formatBytes(speed.maxUploadBytes()) + "/s"); 41 | if (speed.uploadDelay() > 0) 42 | System.out.println(" with " + speed.uploadDelay() + "ms latency"); 43 | else 44 | System.out.println("."); 45 | } 46 | if (speed.maxDownloadBytes() >= 0) { 47 | System.out.print("The download speed is limited to " + SpeedDefinition.formatBytes(speed.maxDownloadBytes()) + "/s"); 48 | if (speed.downloadDelay() > 0) 49 | System.out.println(" with " + speed.downloadDelay() + "ms latency"); 50 | else 51 | System.out.println("."); 52 | } 53 | if (!speed.separateBandwidths()) 54 | System.out.println("Upload and download share the same maximum bandwith."); 55 | 56 | Throttler uploadThrottler = new Throttler(speed.maxUploadBytes(), 1000); 57 | Throttler downloadThrottler = speed.separateBandwidths() ? new Throttler(speed.maxDownloadBytes(), 1000) : uploadThrottler; 58 | 59 | while (true) { 60 | acceptSingle(server, uploadThrottler, downloadThrottler); 61 | } 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | 67 | private void acceptSingle(ServerSocket server, Throttler uploadThrottler, Throttler downloadThrottler) { 68 | try { 69 | final Socket localSocket = server.accept(); 70 | try { 71 | SpeedDefinition speed = options.speed(); 72 | 73 | System.out.println("Accepting: " + localSocket.getRemoteSocketAddress()); 74 | final Socket remoteSocket = options.remote().get(options.balance() ? currentHost.getAndIncrement() : currentHost.get()).newSocket(); 75 | 76 | connected.incrementAndGet(); 77 | System.out.println("Accepted. From: " + localSocket.getRemoteSocketAddress() + ". To: " + remoteSocket.getRemoteSocketAddress() + "."); 78 | 79 | OutputStream remoteOut = remoteSocket.getOutputStream(); 80 | if (speed.uploadDelay() > 0) 81 | remoteOut = new DelayedOutputStream(remoteOut, speed.uploadDelay(), speed.uploadBufferSize()); 82 | 83 | TransferTask upload = new TransferTask( 84 | localSocket.getInputStream(), 85 | remoteOut, 86 | remoteSocket, 87 | uploadThrottler, totalUpload); 88 | 89 | OutputStream localOut = localSocket.getOutputStream(); 90 | if (speed.downloadDelay() > 0) 91 | localOut = new DelayedOutputStream(localOut, speed.downloadDelay(), speed.downloadBufferSize()); 92 | TransferTask download = new TransferTask( 93 | remoteSocket.getInputStream(), 94 | localOut, 95 | remoteSocket, 96 | downloadThrottler, totalDownload); 97 | 98 | Future uploadFuture = executor.submit(upload); 99 | Future downloadFuture = executor.submit(download); 100 | executor.submit(new CloseTask(uploadFuture, downloadFuture, localSocket, remoteSocket)); 101 | } catch (Throwable e) { 102 | e.printStackTrace(); 103 | Util.silentlyClose(localSocket); 104 | } 105 | } catch (Throwable e) { 106 | e.printStackTrace(); 107 | } 108 | } 109 | 110 | private class CloseTask implements Runnable { 111 | private final Future uploadFuture; 112 | private final Future downloadFuture; 113 | private final Socket localSocket; 114 | private final Socket remoteSocket; 115 | 116 | public CloseTask(Future uploadFuture, Future downloadFuture, Socket localSocket, Socket remoteSocket) { 117 | this.uploadFuture = uploadFuture; 118 | this.downloadFuture = downloadFuture; 119 | this.localSocket = localSocket; 120 | this.remoteSocket = remoteSocket; 121 | } 122 | 123 | @Override 124 | public void run() { 125 | Util.getSilently(uploadFuture); 126 | Util.getSilently(downloadFuture); 127 | Util.silentlyClose(localSocket); 128 | Util.silentlyClose(remoteSocket); 129 | System.out.println("Closed: " + localSocket.getRemoteSocketAddress()); 130 | connected.decrementAndGet(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/SpeedDefinition.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class SpeedDefinition { 7 | public static final String[] LEVEL_STRINGS = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; 8 | public static final SpeedDefinition NO_LIMIT = new SpeedDefinition(-1, -1, 0, 0, false); 9 | private final int maxDownloadBytes; 10 | private final int maxUploadBytes; 11 | private final int uploadDelay; 12 | private final int downloadDelay; 13 | private final boolean splitUpDown; 14 | 15 | public SpeedDefinition(int maxDownloadBytes, int maxUploadBytes, int downloadDelay, int uploadDelay, boolean splitUpDown) { 16 | this.maxDownloadBytes = maxDownloadBytes; 17 | this.maxUploadBytes = maxUploadBytes; 18 | this.uploadDelay = uploadDelay; 19 | this.downloadDelay = downloadDelay; 20 | this.splitUpDown = splitUpDown; 21 | 22 | checkBufferSize(makeBufferSize(this.uploadDelay, this.maxUploadBytes), "upload"); 23 | checkBufferSize(makeBufferSize(this.downloadDelay, this.maxDownloadBytes), "download"); 24 | } 25 | 26 | private void checkBufferSize(long size, String type) { 27 | if (size < 0 || size > 128 * 1024 * 1024) 28 | throw new IllegalArgumentException("The specified " + type + " speed and delay require too much memory to be simulated: " + formatBytes(size)); 29 | } 30 | 31 | public static SpeedDefinition parse(String input) { 32 | Pattern pattern = Pattern.compile("(\\d+)([a-z,A-Z]*)(?:\\:(\\d+)([a-z,A-Z]*))?(?:/(\\d+)([a-z,A-Z]*)(?:\\:(\\d+)([a-z,A-Z]*))?)?"); 33 | Matcher matcher = pattern.matcher(input); 34 | 35 | if (!matcher.matches()) 36 | throw new IllegalArgumentException("Invalid speed definition: " + input); 37 | 38 | int downloadBytes = makeSpeed(matcher.group(1), matcher.group(2)); 39 | int downloadDelay = makeDelay(matcher.group(3), matcher.group(4)); 40 | boolean splitUpDown = matcher.group(5) != null; 41 | int uploadBytes = splitUpDown ? makeSpeed(matcher.group(5), matcher.group(6)) : downloadBytes; 42 | int uploadDelay = splitUpDown ? makeDelay(matcher.group(7), matcher.group(8)) : downloadDelay; 43 | 44 | return new SpeedDefinition(downloadBytes, uploadBytes, downloadDelay, uploadDelay, splitUpDown); 45 | } 46 | 47 | private static int makeSpeed(String number, String unit) { 48 | long value = Integer.parseInt(number); 49 | if ("bps".equalsIgnoreCase(unit) || "".equals(unit)) 50 | return (int) (value / 8); 51 | switch (Character.toLowerCase(unit.charAt(0))) { 52 | case 'g': 53 | value *= 1024; 54 | case 'm': 55 | value *= 1024; 56 | case 'k': 57 | value *= 1024; 58 | } 59 | if ((value / 8) > Integer.MAX_VALUE) 60 | throw new IllegalArgumentException("Invalid speed: " + formatBytes(value / 8) + "/s"); 61 | return (int) (value / 8); 62 | } 63 | 64 | private static int makeDelay(String number, String unit) { 65 | if (number == null) return 0; 66 | int value = Integer.parseInt(number); 67 | if ("ms".equalsIgnoreCase(unit)) return value; 68 | if ("s".equalsIgnoreCase(unit)) return value * 1000; 69 | throw new IllegalArgumentException("Invalid delay unit: " + unit); 70 | } 71 | 72 | public int maxDownloadBytes() { 73 | return maxDownloadBytes; 74 | } 75 | 76 | public int maxUploadBytes() { 77 | return maxUploadBytes; 78 | } 79 | 80 | public int uploadDelay() { 81 | return uploadDelay; 82 | } 83 | 84 | public int downloadDelay() { 85 | return downloadDelay; 86 | } 87 | 88 | public boolean separateBandwidths() { 89 | return splitUpDown; 90 | } 91 | 92 | public static long makeBufferSize(int delay, int bytes) { 93 | return (delay + 999L) / 1000 * bytes; 94 | } 95 | 96 | public int uploadBufferSize() { 97 | return (int) makeBufferSize(uploadDelay, maxUploadBytes); 98 | } 99 | 100 | public int downloadBufferSize() { 101 | return (int) makeBufferSize(downloadDelay, maxDownloadBytes); 102 | } 103 | 104 | public static String formatBytes(double bytes) { 105 | if (bytes < 0) return "[virtually infinite]"; 106 | int level = 0; 107 | while (bytes > 1000) { 108 | bytes /= 1024; 109 | level++; 110 | } 111 | return level > 0 ? 112 | String.format("%.2f%s", bytes, LEVEL_STRINGS[level]) : 113 | String.format("%.0fB", bytes); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/StatusTask.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | import static net.intelie.slowproxy.SpeedDefinition.formatBytes; 6 | 7 | public class StatusTask implements Runnable { 8 | private final AtomicLong totalUpload; 9 | private final AtomicLong totalDownload; 10 | private final AtomicLong connected; 11 | private volatile long lastUpload = 0, lastDownload = 0; 12 | 13 | public StatusTask(AtomicLong totalUpload, AtomicLong totalDownload, AtomicLong connected) { 14 | this.totalUpload = totalUpload; 15 | this.totalDownload = totalDownload; 16 | this.connected = connected; 17 | } 18 | 19 | @Override 20 | public void run() { 21 | try { 22 | long currentUpload = totalUpload.get(), currentDownload = totalDownload.get(), currentConnected = connected.get(); 23 | 24 | if (currentDownload != lastDownload || currentUpload != lastUpload || currentConnected > 0) 25 | System.out.println(String.format("%s/s up / %s/s down. Total: %s up / %s down / %d connected", 26 | formatBytes(currentUpload - lastUpload), formatBytes(currentDownload - lastDownload), 27 | formatBytes(currentUpload), formatBytes(currentDownload), 28 | currentConnected)); 29 | lastUpload = currentUpload; 30 | lastDownload = currentDownload; 31 | } catch (Exception e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/Throttler.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.Serializable; 4 | 5 | public class Throttler implements Serializable { 6 | private final long[] queue; 7 | private final int maxCount; 8 | private final long period; 9 | private final int bufferSize; 10 | private final int maxBytes; 11 | private int begin = 0, count = 0; 12 | 13 | public Throttler(int maxBytes, long period) { 14 | this.maxBytes = maxBytes; 15 | this.bufferSize = maxBytes > 0 ? Math.max(maxBytes / 1024, 1) : 1024; 16 | this.maxCount = Math.max(maxBytes / bufferSize, 1); 17 | this.period = period; 18 | this.queue = new long[maxCount]; 19 | } 20 | 21 | public long offer(long ts) { 22 | if (maxBytes < 0) return 0; 23 | if (maxBytes == 0) return period; 24 | return syncOffer(ts); 25 | } 26 | 27 | public byte[] newBuffer() { 28 | return new byte[bufferSize]; 29 | } 30 | 31 | private void update(long now) { 32 | while (count > 0 && queue[begin] <= now) { 33 | begin = (begin + 1) % maxCount; 34 | count--; 35 | } 36 | } 37 | 38 | private synchronized long syncOffer(long ts) { 39 | update(ts); 40 | if (count >= maxCount) 41 | return queue[begin] - ts; 42 | queue[(begin + count++) % maxCount] = ts + period; 43 | return 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/TransferTask.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.io.InputStream; 4 | import java.io.OutputStream; 5 | import java.net.Socket; 6 | import java.net.SocketException; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | class TransferTask implements Runnable { 10 | private final InputStream input; 11 | private final OutputStream output; 12 | private final Socket outputSocket; 13 | private final Throttler throttler; 14 | private final AtomicLong bytes; 15 | 16 | public TransferTask(InputStream input, OutputStream output, Socket outputSocket, Throttler throttler, AtomicLong bytes) { 17 | this.input = input; 18 | this.output = output; 19 | this.outputSocket = outputSocket; 20 | this.throttler = throttler; 21 | this.bytes = bytes; 22 | } 23 | 24 | @Override 25 | public void run() { 26 | try { 27 | byte[] buffer = throttler.newBuffer(); 28 | int len; 29 | while ((len = input.read(buffer)) >= 0) { 30 | long wait = throttler.offer(System.currentTimeMillis()); 31 | while (wait > 0) { 32 | Thread.sleep(wait); 33 | wait = throttler.offer(System.currentTimeMillis()); 34 | } 35 | output.write(buffer, 0, len); 36 | bytes.addAndGet(len); 37 | } 38 | output.flush(); 39 | outputSocket.shutdownOutput(); 40 | } catch (SocketException e) { 41 | if (!"Socket closed".equals(e.getMessage()) && !"Socket output is already shutdown".equals(e.getMessage())) 42 | e.printStackTrace(); 43 | } catch (Throwable e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/intelie/slowproxy/Util.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import java.net.Socket; 4 | import java.util.concurrent.Future; 5 | public class Util { 6 | public static void silentlyClose(Socket localSocket) { 7 | try { 8 | localSocket.close(); 9 | } catch (Throwable ignored) { 10 | 11 | } 12 | } 13 | 14 | public static V getSilently(Future future) { 15 | boolean interrupted = false; 16 | try { 17 | while (true) { 18 | try { 19 | return future.get(); 20 | } catch (InterruptedException e) { 21 | interrupted = true; 22 | } 23 | } 24 | } catch (Throwable ignored) { 25 | return null; 26 | } finally { 27 | if (interrupted) { 28 | Thread.currentThread().interrupt(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/usage.txt: -------------------------------------------------------------------------------- 1 | Slow Proxy v${project.version} 2 | Usage: slowproxy [@] [:...] 3 | Examples: 4 | 5 | $ slowproxy @56kbps 1234 somehost:5678 6 | Proxies requests from localhost:1234 to somehost:5678, 7 | limited to 56kbps 8 | 9 | $ slowproxy @2mbps/56kbps 1234 somehost:5678 10 | Proxies requests from localhost:1234 to somehost:5678, 11 | limited to 2mbps download and 56kbps upload 12 | 13 | $ slowproxy @2mbps:200ms 1234 somehost:5678 14 | Proxies requests from localhost:1234 to somehost:5678, 15 | limited to 2mbps download with a fixed 200ms latency 16 | both uploading and downloading 17 | -------------------------------------------------------------------------------- /src/misc/stub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # simple script for turning a jar with a net.intelie.throttler.Main-Class 3 | # into a stand alone executable 4 | # cat [your jar file] >> [this file] 5 | # then chmod +x [this file] 6 | # you can now exec [this file] 7 | cd "`dirname $0`" 8 | exec java -jar "`basename $0`" "$@" 9 | -------------------------------------------------------------------------------- /src/test/java/net/intelie/slowproxy/DelayedOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import org.junit.Ignore; 4 | import org.junit.Test; 5 | 6 | @Ignore 7 | public class DelayedOutputStreamTest { 8 | @Test 9 | public void testName() throws Exception { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /src/test/java/net/intelie/slowproxy/OptionsTest.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.fest.assertions.Assertions.assertThat; 6 | 7 | public class OptionsTest { 8 | @Test 9 | public void test_56k_1234_blabla_5678() throws Exception { 10 | Options options = Options.parse("@56k", "1234", "blabla:5678"); 11 | 12 | assertThat(options.balance()).isFalse(); 13 | 14 | assertThat(options.speed().maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 15 | assertThat(options.speed().maxUploadBytes()).isEqualTo(56 * 1024 / 8); 16 | 17 | assertThat(options.local().host()).isEqualTo(null); 18 | assertThat(options.local().port()).isEqualTo(1234); 19 | 20 | assertThat(options.remote().size()).isEqualTo(1); 21 | assertThat(options.remote().get(0).host()).isEqualTo("blabla"); 22 | assertThat(options.remote().get(0).port()).isEqualTo(5678); 23 | } 24 | 25 | @Test 26 | public void test_balance() throws Exception { 27 | Options options = Options.parse("balance", "@56k", "1234", "blabla:5678"); 28 | 29 | assertThat(options.balance()).isTrue(); 30 | } 31 | 32 | @Test 33 | public void testDefineLocalAddress() throws Exception { 34 | Options options = Options.parse("@56k", "127.0.0.1:1234", "blabla:5678"); 35 | 36 | assertThat(options.speed().maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 37 | assertThat(options.speed().maxUploadBytes()).isEqualTo(56 * 1024 / 8); 38 | 39 | assertThat(options.local().host()).isEqualTo("127.0.0.1"); 40 | assertThat(options.local().port()).isEqualTo(1234); 41 | 42 | assertThat(options.remote().size()).isEqualTo(1); 43 | assertThat(options.remote().get(0).host()).isEqualTo("blabla"); 44 | assertThat(options.remote().get(0).port()).isEqualTo(5678); 45 | } 46 | 47 | @Test 48 | public void testNoSpeed() throws Exception { 49 | Options options = Options.parse("1234", "blabla:5678"); 50 | 51 | assertThat(options.speed().maxDownloadBytes()).isEqualTo(-1); 52 | assertThat(options.speed().maxUploadBytes()).isEqualTo(-1); 53 | 54 | assertThat(options.local().host()).isEqualTo(null); 55 | assertThat(options.local().port()).isEqualTo(1234); 56 | 57 | assertThat(options.remote().size()).isEqualTo(1); 58 | assertThat(options.remote().get(0).host()).isEqualTo("blabla"); 59 | assertThat(options.remote().get(0).port()).isEqualTo(5678); 60 | } 61 | 62 | @Test 63 | public void testMultipleHosts() throws Exception { 64 | Options options = Options.parse("1234", "blabla:5678", "xxx:4567"); 65 | 66 | assertThat(options.speed().maxDownloadBytes()).isEqualTo(-1); 67 | assertThat(options.speed().maxUploadBytes()).isEqualTo(-1); 68 | 69 | assertThat(options.local().host()).isEqualTo(null); 70 | assertThat(options.local().port()).isEqualTo(1234); 71 | 72 | assertThat(options.remote().size()).isEqualTo(2); 73 | assertThat(options.remote().get(0).host()).isEqualTo("blabla"); 74 | assertThat(options.remote().get(0).port()).isEqualTo(5678); 75 | 76 | assertThat(options.remote().get(1).host()).isEqualTo("xxx"); 77 | assertThat(options.remote().get(1).port()).isEqualTo(4567); 78 | } 79 | } -------------------------------------------------------------------------------- /src/test/java/net/intelie/slowproxy/SpeedDefinitionTest.java: -------------------------------------------------------------------------------- 1 | package net.intelie.slowproxy; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.fest.assertions.Assertions.assertThat; 6 | 7 | public class SpeedDefinitionTest { 8 | @Test 9 | public void test56kbps() throws Exception { 10 | SpeedDefinition def = SpeedDefinition.parse("56k"); 11 | 12 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 13 | assertThat(def.maxUploadBytes()).isEqualTo(56 * 1024 / 8); 14 | assertThat(def.uploadDelay()).isEqualTo(0); 15 | assertThat(def.downloadDelay()).isEqualTo(0); 16 | assertThat(def.separateBandwidths()).isFalse(); 17 | } 18 | 19 | @Test 20 | public void test56kbps200ms() throws Exception { 21 | SpeedDefinition def = SpeedDefinition.parse("56k:200ms"); 22 | 23 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 24 | assertThat(def.maxUploadBytes()).isEqualTo(56 * 1024 / 8); 25 | assertThat(def.uploadDelay()).isEqualTo(200); 26 | assertThat(def.downloadDelay()).isEqualTo(200); 27 | assertThat(def.separateBandwidths()).isFalse(); 28 | } 29 | 30 | @Test 31 | public void test56M() throws Exception { 32 | SpeedDefinition def = SpeedDefinition.parse("56Mbps"); 33 | 34 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 * 1024 / 8); 35 | assertThat(def.maxUploadBytes()).isEqualTo(56 * 1024 * 1024 / 8); 36 | assertThat(def.uploadDelay()).isEqualTo(0); 37 | assertThat(def.downloadDelay()).isEqualTo(0); 38 | assertThat(def.separateBandwidths()).isFalse(); 39 | } 40 | 41 | @Test 42 | public void test56M2seconds() throws Exception { 43 | SpeedDefinition def = SpeedDefinition.parse("56Mbps:2s"); 44 | 45 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 * 1024 / 8); 46 | assertThat(def.maxUploadBytes()).isEqualTo(56 * 1024 * 1024 / 8); 47 | assertThat(def.uploadDelay()).isEqualTo(2000); 48 | assertThat(def.downloadDelay()).isEqualTo(2000); 49 | assertThat(def.separateBandwidths()).isFalse(); 50 | } 51 | 52 | @Test 53 | public void test56kbpsDown2mbpsUp() throws Exception { 54 | SpeedDefinition def = SpeedDefinition.parse("56kbps/2mbps"); 55 | 56 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 57 | assertThat(def.maxUploadBytes()).isEqualTo(2 * 1024 * 1024 / 8); 58 | assertThat(def.uploadDelay()).isEqualTo(0); 59 | assertThat(def.downloadDelay()).isEqualTo(0); 60 | assertThat(def.separateBandwidths()).isTrue(); 61 | } 62 | 63 | @Test 64 | public void test56kbpsDown2mbpsUpWithDelay() throws Exception { 65 | SpeedDefinition def = SpeedDefinition.parse("56kbps:2s/2mbps:1ms"); 66 | 67 | assertThat(def.maxDownloadBytes()).isEqualTo(56 * 1024 / 8); 68 | assertThat(def.maxUploadBytes()).isEqualTo(2 * 1024 * 1024 / 8); 69 | assertThat(def.uploadDelay()).isEqualTo(1); 70 | assertThat(def.downloadDelay()).isEqualTo(2000); 71 | assertThat(def.separateBandwidths()).isTrue(); 72 | } 73 | 74 | @Test 75 | public void testBufferSize() throws Exception { 76 | SpeedDefinition def = SpeedDefinition.parse("56kbps:1500ms/2mbps:1ms"); 77 | 78 | assertThat(def.downloadBufferSize()).isEqualTo(112 * 1024 / 8); 79 | assertThat(def.uploadBufferSize()).isEqualTo(2 * 1024 * 1024 / 8); 80 | } 81 | 82 | @Test 83 | public void testBufferSizeFull() throws Exception { 84 | SpeedDefinition def = SpeedDefinition.parse("56kbps:2000ms/2mbps:1000ms"); 85 | 86 | assertThat(def.downloadBufferSize()).isEqualTo(112 * 1024 / 8); 87 | assertThat(def.uploadBufferSize()).isEqualTo(2 * 1024 * 1024 / 8); 88 | } 89 | } --------------------------------------------------------------------------------