Skip to content

Commit

Permalink
ProxyAuthenticator enhancements (adamfisk#8)
Browse files Browse the repository at this point in the history
* Changed ProxyAuthenticator to be more generic. It accepts AuthorizationHeader and HttpRequest

* Refactored a bit
  • Loading branch information
k-sever committed Mar 2, 2018
1 parent ee28070 commit ff7f287
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.littleshoot.proxy;

import org.littleshoot.proxy.impl.ProxyUtils;

import java.util.Date;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;

public abstract class AbstractProxyAuthenticator implements ProxyAuthenticator {

@Override
public abstract boolean authenticate(HttpRequest request);

@Override
public abstract String getRealm();

@Override
public FullHttpResponse authenticationFailureResponse(HttpRequest request) {
String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n"
+ "<html><head>\n"
+ "<title>407 Proxy Authentication Required</title>\n"
+ "</head><body>\n"
+ "<h1>Proxy Authentication Required</h1>\n"
+ "<p>This server could not verify that you\n"
+ "are authorized to access the document\n"
+ "requested. Either you supplied the wrong\n"
+ "credentials (e.g., bad password), or your\n"
+ "browser doesn't understand how to supply\n"
+ "the credentials required.</p>\n" + "</body></html>\n";
FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED, body);
HttpHeaders.setDate(response, new Date());
response.headers().set("Proxy-Authenticate", "Basic realm=\"" + (getRealm() == null ? "Restricted Files" : getRealm()) + "\"");
return response;
}
}
47 changes: 47 additions & 0 deletions src/main/java/org/littleshoot/proxy/BasicProxyAuthenticator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.littleshoot.proxy;

import com.google.common.io.BaseEncoding;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.util.List;

import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;

/**
* Basic Auth user/password authenticator
*/
public abstract class BasicProxyAuthenticator extends AbstractProxyAuthenticator {

private static final Logger LOG = LoggerFactory.getLogger(BasicProxyAuthenticator.class);

@Override
public boolean authenticate(HttpRequest request) {
if (!request.headers().contains(HttpHeaders.Names.PROXY_AUTHORIZATION)) {
return false;
}

List<String> values = request.headers().getAll(HttpHeaders.Names.PROXY_AUTHORIZATION);
String fullValue = values.iterator().next();
String value = StringUtils.substringAfter(fullValue, "Basic ").trim();

if (StringUtils.isNotEmpty(value)) {
byte[] decodedValue = BaseEncoding.base64().decode(value);
String decodedString = new String(decodedValue, Charset.forName("UTF-8"));

String userName = StringUtils.substringBefore(decodedString, ":");
String password = StringUtils.substringAfter(decodedString, ":");

return authenticate(userName, password, request);
}

LOG.debug("Invalid authentication scheme. Expected 'Basic'");
return false;
}

public abstract boolean authenticate(String username, String password, HttpRequest request);
}
28 changes: 18 additions & 10 deletions src/main/java/org/littleshoot/proxy/ProxyAuthenticator.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package org.littleshoot.proxy;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;

/**
* Interface for objects that can authenticate someone for using our Proxy on
* the basis of a username and password.
* the basis of authorization header.
*/
public interface ProxyAuthenticator {

/**
* Authenticates the user using the specified userName and password.
*
* @param userName
* The user name.
* @param password
* The password.
* @return <code>true</code> if the credentials are acceptable, otherwise
* Authenticates the user using the specified proxy authorization header.
*
* @param httpRequest
* http request
*
* @return <code>true</code> if the credential is acceptable, otherwise
* <code>false</code>.
*/
boolean authenticate(String userName, String password);
boolean authenticate(HttpRequest httpRequest);

/**
* The realm value to be used in the request for proxy authentication
* ("Proxy-Authenticate" header). Returning null will cause the string
Expand All @@ -25,4 +28,9 @@ public interface ProxyAuthenticator {
* @return
*/
String getRealm();

/**
* Response that is going to be returned on authentication failure
*/
FullHttpResponse authenticationFailureResponse(HttpRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.littleshoot.proxy.impl;

import com.google.common.io.BaseEncoding;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
Expand Down Expand Up @@ -39,7 +38,6 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -996,27 +994,12 @@ private boolean authenticationRequired(HttpRequest request) {
final ProxyAuthenticator authenticator = proxyServer
.getProxyAuthenticator();

if (authenticator == null)
if (authenticator == null) {
return false;

if (!request.headers().contains(HttpHeaders.Names.PROXY_AUTHORIZATION)) {
writeAuthenticationRequired(authenticator.getRealm());
return true;
}

List<String> values = request.headers().getAll(
HttpHeaders.Names.PROXY_AUTHORIZATION);
String fullValue = values.iterator().next();
String value = StringUtils.substringAfter(fullValue, "Basic ").trim();

byte[] decodedValue = BaseEncoding.base64().decode(value);

String decodedString = new String(decodedValue, Charset.forName("UTF-8"));

String userName = StringUtils.substringBefore(decodedString, ":");
String password = StringUtils.substringAfter(decodedString, ":");
if (!authenticator.authenticate(userName, password)) {
writeAuthenticationRequired(authenticator.getRealm());
if (!authenticator.authenticate(request)) {
write(authenticator.authenticationFailureResponse(request));
return true;
}

Expand All @@ -1030,26 +1013,6 @@ private boolean authenticationRequired(HttpRequest request) {
return false;
}

private void writeAuthenticationRequired(String realm) {
String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n"
+ "<html><head>\n"
+ "<title>407 Proxy Authentication Required</title>\n"
+ "</head><body>\n"
+ "<h1>Proxy Authentication Required</h1>\n"
+ "<p>This server could not verify that you\n"
+ "are authorized to access the document\n"
+ "requested. Either you supplied the wrong\n"
+ "credentials (e.g., bad password), or your\n"
+ "browser doesn't understand how to supply\n"
+ "the credentials required.</p>\n" + "</body></html>\n";
FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED, body);
HttpHeaders.setDate(response, new Date());
response.headers().set("Proxy-Authenticate",
"Basic realm=\"" + (realm == null ? "Restricted Files" : realm) + "\"");
write(response);
}

/***************************************************************************
* Request/Response Rewriting
**************************************************************************/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
* uses MITM.
*/
public class MITMUsernamePasswordAuthenticatingProxyTest extends
UsernamePasswordAuthenticatingProxyTest
implements ProxyAuthenticator {
UsernamePasswordAuthenticatingProxyTest {
@Override
protected void setUp() {
this.proxyServer = bootstrapProxy()
.withPort(0)
.withProxyAuthenticator(this)
.withProxyAuthenticator(new TestBasicProxyAuthenticator())
.withManInTheMiddle(new SelfSignedMitmManagerFactory())
.start();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,47 @@
package org.littleshoot.proxy;

import io.netty.handler.codec.http.HttpRequest;

/**
* Tests a single proxy that requires username/password authentication.
*/
public class UsernamePasswordAuthenticatingProxyTest extends BaseProxyTest
implements ProxyAuthenticator {
public class UsernamePasswordAuthenticatingProxyTest extends BaseProxyTest {
private static String USERNAME = "user1";
private static String PASSWORD = "password";

@Override
protected void setUp() {
this.proxyServer = bootstrapProxy()
.withPort(0)
.withProxyAuthenticator(this)
.withProxyAuthenticator(new TestBasicProxyAuthenticator())
.start();
}

@Override
protected String getUsername() {
return "user1";
return USERNAME;
}

@Override
protected String getPassword() {
return "user2";
}

@Override
public boolean authenticate(String userName, String password) {
return getUsername().equals(userName) && getPassword().equals(password);
return PASSWORD;
}

@Override
protected boolean isAuthenticating() {
return true;
}

@Override
public String getRealm() {
return null;
static class TestBasicProxyAuthenticator extends BasicProxyAuthenticator {

@Override
public String getRealm() {
return null;
}

@Override
public boolean authenticate(String username, String password, HttpRequest request) {
return USERNAME.equals(username) && PASSWORD.equals(password);
}
}
}

0 comments on commit ff7f287

Please sign in to comment.