<commit_before>/*
 * Portions Copyright (c) 2009-10 Zepheira LLC, Some Rights Reserved
 * Portions Copyright (c) 2010-11 Talis Inc, Some Rights Reserved
 *
 * 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 org.callimachusproject.auth;

import static org.openrdf.query.QueryLanguage.SPARQL;
import info.aduna.net.ParsedURI;

import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.tools.FileObject;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateUtil;
import org.apache.http.HttpEntity;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.callimachusproject.server.exceptions.BadRequest;
import org.callimachusproject.server.exceptions.InternalServerError;
import org.callimachusproject.setup.SecretOriginProvider;
import org.callimachusproject.traits.VersionedObject;
import org.callimachusproject.util.PasswordGenerator;
import org.openrdf.OpenRDFException;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.RDFS;
import org.openrdf.model.vocabulary.XMLSchema;
import org.openrdf.query.BindingSet;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.query.Update;
import org.openrdf.query.algebra.evaluation.util.ValueComparator;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.RDFObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Validates HTTP digest authorization.
 */
public class DigestDetachedManager implements DetachedAuthenticationManager {
	private static final String DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";
	private static final String FOAF_DEPICTION = "http://xmlns.com/foaf/0.1/depiction";
	private static final String HAS_COMPONENT = "http://callimachusproject.org/rdf/2009/framework#hasComponent";
	private static final String PARTY = "http://callimachusproject.org/rdf/2009/framework#Party";
	private static final String USER = "http://callimachusproject.org/rdf/2009/framework#User";
	private static final String EMAIL = "http://callimachusproject.org/rdf/2009/framework#email";
	private static final String PROV = "http://www.w3.org/ns/prov#";
	private static final String PREFIX = "PREFIX calli:<http://callimachusproject.org/rdf/2009/framework#>\n";
	private static final String SELECT_PASSWORD = PREFIX
			+ "SELECT (str(?user) AS ?id) ?encoded ?passwordDigest {{\n"
			+ "?user calli:name $name .\n"
			+ "$this calli:authNamespace ?folder .\n"
			+ "?folder calli:hasComponent ?user .\n"
			+ "FILTER (str(?user) = concat(str(?folder), $name))\n"
			+ "OPTIONAL { ?user calli:encoded ?encoded; calli:algorithm \"MD5\" }\n"
			+ "OPTIONAL { ?user calli:passwordDigest ?passwordDigest }\n"
			+ "} UNION {\n" + "?user calli:email $name .\n"
			+ "$this calli:authNamespace ?folder .\n"
			+ "?folder calli:hasComponent ?user .\n"
			+ "OPTIONAL { ?user calli:passwordDigest ?passwordDigest }\n"
			+ "}}";
	private static final String COPY_PERM = PREFIX
			+ "INSERT { $dst\n"
			+ "calli:reader ?reader; calli:subscriber ?subscriber; calli:contributor ?contributor; calli:editor ?editor; calli:administrator ?administrator\n"
			+ "} WHERE { { $src calli:reader ?reader }\n"
			+ "UNION { $src calli:subscriber ?subscriber }\n"
			+ "UNION { $src calli:contributor ?contributor }\n"
			+ "UNION { $src calli:editor ?editor }\n"
			+ "UNION { $src calli:administrator ?administrator }\n" + "}";
	private static final Pattern TOKENS_REGEX = Pattern
			.compile("\\s*([\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\.\\^\\_\\`\\~]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|([^,\"]*)))?\\s*,?");
	private static final long THREE_MONTHS = 3 * 30 * 24 * 60 * 60;
	private static final int MAX_NONCE_AGE = 300000; // nonce timeout of 5min
	private static final BasicStatusLine _403 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 403, "Forbidden");
	private static final BasicStatusLine _401 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 401, "Unauthorized");
	private static final BasicStatusLine _204 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 204, "No Content");
	private static final BasicStatusLine _200 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 200, "OK");
	private static final Map<String, String> DIGEST_OPTS = new HashMap<String, String>();
	static {
		DIGEST_OPTS.put("realm", null);
		DIGEST_OPTS.put("nonce", null);
		DIGEST_OPTS.put("username", null);
		DIGEST_OPTS.put("uri", null);
		DIGEST_OPTS.put("qop", null);
		DIGEST_OPTS.put("cnonce", null);
		DIGEST_OPTS.put("nc", null);
		DIGEST_OPTS.put("response", null);
	}
	private final Logger logger = LoggerFactory.getLogger(DigestDetachedManager.class);
	private final Resource self;
	private final String authName;
	private final String protectedDomains;
	private final String protectedPath;
	private final RealmManager realms;
	private final FailManager fail = new FailManager();
	private final String digestNonceSecure;
	private final String digestNonce;
	private final Set<String> userCookies = new LinkedHashSet<String>();
	private final AuthorizationService service = AuthorizationService
			.getInstance();

	public DigestDetachedManager(Resource self, String authName, String path,
			List<String> domains, RealmManager realms) {
		assert self != null;
		assert authName != null;
		assert realms != null;
		this.realms = realms;
		this.self = self;
		this.authName = authName;
		this.protectedPath = path;
		assert domains != null;
		assert domains.size() > 0;
		boolean secureOnly = true;
		Set<Integer> ports = new HashSet<Integer>();
		StringBuilder sb = new StringBuilder();
		for (String domain : domains) {
			sb.append(' ').append(domain);
			int port = java.net.URI.create(domain).getPort();
			ports.add(port);
			StringBuilder suffix = new StringBuilder();
			if (port > 0) {
				suffix.append(port);
			}
			if (domain.startsWith("https")) {
				suffix.append('s');
			} else {
				secureOnly = false;
			}
			suffix.append('=');
			userCookies.add("username" + suffix);
		}
		this.protectedDomains = sb.substring(1);
		this.digestNonceSecure = secureOnly ? ";Secure" : "";
		StringBuilder dn = new StringBuilder();
		dn.append("digestNonce");
		dn.append(Integer.toHexString(Math.abs(self.hashCode())));
		if (ports.size() == 1) {
			Integer port = ports.iterator().next();
			if (port > 0) {
				dn.append(port);
			}
		}
		if (secureOnly) {
			dn.append('s');
		}
		dn.append("=");
		this.digestNonce = dn.toString();
	}

	@Override
	public String toString() {
		return getIdentifier();
	}

	@Override
	public String getIdentifier() {
		return self.stringValue();
	}

	@Override
	public HttpResponse unauthorized(String method, Object resource,
			Map<String, String[]> request, HttpEntity body) {
		String nonce = nextNonce(resource, request.get("via"));
		String authenticate = "Digest realm=\"" + authName + "\""
				+ (", domain=\"" + protectedDomains + "\"") + ", nonce=\""
				+ nonce + "\", algorithm=\"MD5\", qop=\"auth\"";
		Map<String, String> options = parseDigestAuthorization(request);
		if (options != null) {
			if (!isRecentDigest(resource, request, options)) {
				authenticate += ",stale=true";
				HttpResponse resp = new BasicHttpResponse(_401);
				resp.setHeader("Cache-Control", "no-store");
				resp.setHeader("Content-Type", "text/plain;charset=\"UTF-8\"");
				resp.setHeader("WWW-Authenticate", authenticate);
				try {
					resp.setEntity(new StringEntity("Stale authorization header", "UTF-8"));
				} catch (UnsupportedEncodingException e) {
					throw new AssertionError(e);
				}
				return resp;
			}
		}
		HttpResponse resp = new BasicHttpResponse(_401);
		resp.setHeader("Cache-Control", "no-store");
		resp.setHeader("Content-Type", "text/plain;charset=\"UTF-8\"");
		resp.setHeader("WWW-Authenticate", authenticate);
		try {
			resp.setEntity(new StringEntity("Unauthorized", "UTF-8"));
		} catch (UnsupportedEncodingException e) {
			throw new AssertionError(e);
		}
		return resp;
	}

	@Override
	public HttpMessage authenticationInfo(String method, Object resource,
			Map<String, String[]> request, ObjectConnection con)
			throws OpenRDFException, IOException {
		Map<String, String> auth = parseDigestAuthorization(request);
		if (auth == null)
			return null;
		Map.Entry<String, String> password = findAuthUser(method, resource,
				request, auth, con);
		if (password == null)
			return null;
		String cnonce = auth.get("cnonce");
		String nc = auth.get("nc");
		String uri = auth.get("uri");
		String nonce = auth.get("nonce");
		String ha1 = password.getKey();
		String ha2 = md5(":" + uri);
		String rspauth = md5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce
				+ ":auth:" + ha2);
		String authenticate = "qop=auth,cnonce=\"" + cnonce + "\",nc=" + nc
				+ ",rspauth=\"" + rspauth + "\"";
		BasicHttpResponse resp = new BasicHttpResponse(_204);
		resp.addHeader("Authentication-Info", authenticate);
		return resp;
	}

	@Override
	public String authenticateRequest(String method, Object resource,
			Map<String, String[]> request, ObjectConnection con) {
		try {
			Map<String, String> options = parseDigestAuthorization(request);
			if (options == null)
				return null;
			Map.Entry<String, String> password = findAuthUser(method, resource,
					request, options, con);
			String username = options.get("username");
			if (password == null) {
				if (isRecentDigest(resource, request, options)) {
					fail.failedAttempt(username);
				}
				return null;
			}
			if (options.containsKey("qop")) {
				if (fail.isReplayed(options)) {
					fail.failedAttempt(username);
					logger.info("Request replayed {}", options);
					return null;
				}
			}
			return password.getValue();
		} catch (BadRequest e) {
			throw e;
		} catch (Exception e) {
			logger.warn(e.toString(), e);
			return null;
		}
	}

	@Override
	public HttpResponse logout(Collection<String> tokens) {
		for (String token : tokens) {
			if (token.indexOf("username=\"-\"") > 0) {
				// # bogus credentials received
				BasicHttpResponse resp = new BasicHttpResponse(_204);
					resp.addHeader("Set-Cookie", digestNonce
							+ ";Max-Age=0;Path=/;HttpOnly" + digestNonceSecure);
				for (String userCookie : userCookies) {
					String secure = userCookie.endsWith("s=") ? ";Secure" : "";
					resp.addHeader("Set-Cookie", userCookie
							+ ";Max-Age=0;Path=" + protectedPath + secure);
				}
				return resp;
			}
		}
		// # the browser must send invalid credentials to logout
		BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1,
				401, "Unauthorized");
		String hd = "Digest realm=\"" + authName + "\", domain=\""
				+ protectedDomains
				+ "\", nonce=\"logout\", algorithm=\"MD5\", qop=\"auth\"";
		resp.setHeader("WWW-Authenticate", hd);
		return resp;
	}

	public HttpResponse login(Collection<String> tokens, boolean persistent,
			ObjectConnection con) throws OpenRDFException, IOException {
		HttpResponse resp = null;
		String username = getUserLogin(tokens, con);
		if (persistent) {
			resp = getPersistentLogin(username, con);
		}
		if (resp == null) {
			resp = new BasicHttpResponse(_204);
		}
		String[] cookies = getUsernameSetCookie(tokens, con);
		if (cookies.length == 0)
			return new BasicHttpResponse(_403);
		for (String cookie : cookies) {
			resp.addHeader("Set-Cookie", cookie);
		}
		return resp;
	}

	public String[] getUsernameSetCookie(Collection<String> tokens,
			ObjectConnection con) {
		String username = getUserLogin(tokens, con);
		if (username == null)
			return new String[0];
		int i = 0;
		String[] result = new String[userCookies.size()];
		for (String userCookie : userCookies) {
			String secure = userCookie.endsWith("s=") ? ";Secure" : "";
			result[i++] = userCookie + encode(username) + ";Path=" + protectedPath + secure;
		}
		return result;
	}

	public String getUserIdentifier(Collection<String> tokens,
			ObjectConnection con) throws OpenRDFException, IOException {<commit_msg>Explicitly set no-cache for POST login response<commit_after>/*
 * Portions Copyright (c) 2009-10 Zepheira LLC, Some Rights Reserved
 * Portions Copyright (c) 2010-11 Talis Inc, Some Rights Reserved
 *
 * 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 org.callimachusproject.auth;

import static org.openrdf.query.QueryLanguage.SPARQL;
import info.aduna.net.ParsedURI;

import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.tools.FileObject;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateUtil;
import org.apache.http.HttpEntity;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.callimachusproject.server.exceptions.BadRequest;
import org.callimachusproject.server.exceptions.InternalServerError;
import org.callimachusproject.setup.SecretOriginProvider;
import org.callimachusproject.traits.VersionedObject;
import org.callimachusproject.util.PasswordGenerator;
import org.openrdf.OpenRDFException;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.RDFS;
import org.openrdf.model.vocabulary.XMLSchema;
import org.openrdf.query.BindingSet;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.query.Update;
import org.openrdf.query.algebra.evaluation.util.ValueComparator;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.RDFObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Validates HTTP digest authorization.
 */
public class DigestDetachedManager implements DetachedAuthenticationManager {
	private static final String DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";
	private static final String FOAF_DEPICTION = "http://xmlns.com/foaf/0.1/depiction";
	private static final String HAS_COMPONENT = "http://callimachusproject.org/rdf/2009/framework#hasComponent";
	private static final String PARTY = "http://callimachusproject.org/rdf/2009/framework#Party";
	private static final String USER = "http://callimachusproject.org/rdf/2009/framework#User";
	private static final String EMAIL = "http://callimachusproject.org/rdf/2009/framework#email";
	private static final String PROV = "http://www.w3.org/ns/prov#";
	private static final String PREFIX = "PREFIX calli:<http://callimachusproject.org/rdf/2009/framework#>\n";
	private static final String SELECT_PASSWORD = PREFIX
			+ "SELECT (str(?user) AS ?id) ?encoded ?passwordDigest {{\n"
			+ "?user calli:name $name .\n"
			+ "$this calli:authNamespace ?folder .\n"
			+ "?folder calli:hasComponent ?user .\n"
			+ "FILTER (str(?user) = concat(str(?folder), $name))\n"
			+ "OPTIONAL { ?user calli:encoded ?encoded; calli:algorithm \"MD5\" }\n"
			+ "OPTIONAL { ?user calli:passwordDigest ?passwordDigest }\n"
			+ "} UNION {\n" + "?user calli:email $name .\n"
			+ "$this calli:authNamespace ?folder .\n"
			+ "?folder calli:hasComponent ?user .\n"
			+ "OPTIONAL { ?user calli:passwordDigest ?passwordDigest }\n"
			+ "}}";
	private static final String COPY_PERM = PREFIX
			+ "INSERT { $dst\n"
			+ "calli:reader ?reader; calli:subscriber ?subscriber; calli:contributor ?contributor; calli:editor ?editor; calli:administrator ?administrator\n"
			+ "} WHERE { { $src calli:reader ?reader }\n"
			+ "UNION { $src calli:subscriber ?subscriber }\n"
			+ "UNION { $src calli:contributor ?contributor }\n"
			+ "UNION { $src calli:editor ?editor }\n"
			+ "UNION { $src calli:administrator ?administrator }\n" + "}";
	private static final Pattern TOKENS_REGEX = Pattern
			.compile("\\s*([\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\.\\^\\_\\`\\~]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|([^,\"]*)))?\\s*,?");
	private static final long THREE_MONTHS = 3 * 30 * 24 * 60 * 60;
	private static final int MAX_NONCE_AGE = 300000; // nonce timeout of 5min
	private static final BasicStatusLine _403 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 403, "Forbidden");
	private static final BasicStatusLine _401 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 401, "Unauthorized");
	private static final BasicStatusLine _204 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 204, "No Content");
	private static final BasicStatusLine _200 = new BasicStatusLine(
			HttpVersion.HTTP_1_1, 200, "OK");
	private static final Map<String, String> DIGEST_OPTS = new HashMap<String, String>();
	static {
		DIGEST_OPTS.put("realm", null);
		DIGEST_OPTS.put("nonce", null);
		DIGEST_OPTS.put("username", null);
		DIGEST_OPTS.put("uri", null);
		DIGEST_OPTS.put("qop", null);
		DIGEST_OPTS.put("cnonce", null);
		DIGEST_OPTS.put("nc", null);
		DIGEST_OPTS.put("response", null);
	}
	private final Logger logger = LoggerFactory.getLogger(DigestDetachedManager.class);
	private final Resource self;
	private final String authName;
	private final String protectedDomains;
	private final String protectedPath;
	private final RealmManager realms;
	private final FailManager fail = new FailManager();
	private final String digestNonceSecure;
	private final String digestNonce;
	private final Set<String> userCookies = new LinkedHashSet<String>();
	private final AuthorizationService service = AuthorizationService
			.getInstance();

	public DigestDetachedManager(Resource self, String authName, String path,
			List<String> domains, RealmManager realms) {
		assert self != null;
		assert authName != null;
		assert realms != null;
		this.realms = realms;
		this.self = self;
		this.authName = authName;
		this.protectedPath = path;
		assert domains != null;
		assert domains.size() > 0;
		boolean secureOnly = true;
		Set<Integer> ports = new HashSet<Integer>();
		StringBuilder sb = new StringBuilder();
		for (String domain : domains) {
			sb.append(' ').append(domain);
			int port = java.net.URI.create(domain).getPort();
			ports.add(port);
			StringBuilder suffix = new StringBuilder();
			if (port > 0) {
				suffix.append(port);
			}
			if (domain.startsWith("https")) {
				suffix.append('s');
			} else {
				secureOnly = false;
			}
			suffix.append('=');
			userCookies.add("username" + suffix);
		}
		this.protectedDomains = sb.substring(1);
		this.digestNonceSecure = secureOnly ? ";Secure" : "";
		StringBuilder dn = new StringBuilder();
		dn.append("digestNonce");
		dn.append(Integer.toHexString(Math.abs(self.hashCode())));
		if (ports.size() == 1) {
			Integer port = ports.iterator().next();
			if (port > 0) {
				dn.append(port);
			}
		}
		if (secureOnly) {
			dn.append('s');
		}
		dn.append("=");
		this.digestNonce = dn.toString();
	}

	@Override
	public String toString() {
		return getIdentifier();
	}

	@Override
	public String getIdentifier() {
		return self.stringValue();
	}

	@Override
	public HttpResponse unauthorized(String method, Object resource,
			Map<String, String[]> request, HttpEntity body) {
		String nonce = nextNonce(resource, request.get("via"));
		String authenticate = "Digest realm=\"" + authName + "\""
				+ (", domain=\"" + protectedDomains + "\"") + ", nonce=\""
				+ nonce + "\", algorithm=\"MD5\", qop=\"auth\"";
		Map<String, String> options = parseDigestAuthorization(request);
		if (options != null) {
			if (!isRecentDigest(resource, request, options)) {
				authenticate += ",stale=true";
				HttpResponse resp = new BasicHttpResponse(_401);
				resp.setHeader("Cache-Control", "no-store");
				resp.setHeader("Content-Type", "text/plain;charset=\"UTF-8\"");
				resp.setHeader("WWW-Authenticate", authenticate);
				try {
					resp.setEntity(new StringEntity("Stale authorization header", "UTF-8"));
				} catch (UnsupportedEncodingException e) {
					throw new AssertionError(e);
				}
				return resp;
			}
		}
		HttpResponse resp = new BasicHttpResponse(_401);
		resp.setHeader("Cache-Control", "no-store");
		resp.setHeader("Content-Type", "text/plain;charset=\"UTF-8\"");
		resp.setHeader("WWW-Authenticate", authenticate);
		try {
			resp.setEntity(new StringEntity("Unauthorized", "UTF-8"));
		} catch (UnsupportedEncodingException e) {
			throw new AssertionError(e);
		}
		return resp;
	}

	@Override
	public HttpMessage authenticationInfo(String method, Object resource,
			Map<String, String[]> request, ObjectConnection con)
			throws OpenRDFException, IOException {
		Map<String, String> auth = parseDigestAuthorization(request);
		if (auth == null)
			return null;
		Map.Entry<String, String> password = findAuthUser(method, resource,
				request, auth, con);
		if (password == null)
			return null;
		String cnonce = auth.get("cnonce");
		String nc = auth.get("nc");
		String uri = auth.get("uri");
		String nonce = auth.get("nonce");
		String ha1 = password.getKey();
		String ha2 = md5(":" + uri);
		String rspauth = md5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce
				+ ":auth:" + ha2);
		String authenticate = "qop=auth,cnonce=\"" + cnonce + "\",nc=" + nc
				+ ",rspauth=\"" + rspauth + "\"";
		BasicHttpResponse resp = new BasicHttpResponse(_204);
		resp.addHeader("Authentication-Info", authenticate);
		return resp;
	}

	@Override
	public String authenticateRequest(String method, Object resource,
			Map<String, String[]> request, ObjectConnection con) {
		try {
			Map<String, String> options = parseDigestAuthorization(request);
			if (options == null)
				return null;
			Map.Entry<String, String> password = findAuthUser(method, resource,
					request, options, con);
			String username = options.get("username");
			if (password == null) {
				if (isRecentDigest(resource, request, options)) {
					fail.failedAttempt(username);
				}
				return null;
			}
			if (options.containsKey("qop")) {
				if (fail.isReplayed(options)) {
					fail.failedAttempt(username);
					logger.info("Request replayed {}", options);
					return null;
				}
			}
			return password.getValue();
		} catch (BadRequest e) {
			throw e;
		} catch (Exception e) {
			logger.warn(e.toString(), e);
			return null;
		}
	}

	@Override
	public HttpResponse logout(Collection<String> tokens) {
		for (String token : tokens) {
			if (token.indexOf("username=\"-\"") > 0) {
				// # bogus credentials received
				BasicHttpResponse resp = new BasicHttpResponse(_204);
					resp.addHeader("Set-Cookie", digestNonce
							+ ";Max-Age=0;Path=/;HttpOnly" + digestNonceSecure);
				for (String userCookie : userCookies) {
					String secure = userCookie.endsWith("s=") ? ";Secure" : "";
					resp.addHeader("Set-Cookie", userCookie
							+ ";Max-Age=0;Path=" + protectedPath + secure);
				}
				return resp;
			}
		}
		// # the browser must send invalid credentials to logout
		BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1,
				401, "Unauthorized");
		String hd = "Digest realm=\"" + authName + "\", domain=\""
				+ protectedDomains
				+ "\", nonce=\"logout\", algorithm=\"MD5\", qop=\"auth\"";
		resp.setHeader("WWW-Authenticate", hd);
		return resp;
	}

	public HttpResponse login(Collection<String> tokens, boolean persistent,
			ObjectConnection con) throws OpenRDFException, IOException {
		HttpResponse resp = null;
		String username = getUserLogin(tokens, con);
		if (persistent) {
			resp = getPersistentLogin(username, con);
		}
		if (resp == null) {
			resp = new BasicHttpResponse(_204);
		}
		resp.addHeader("Cache-Control", "no-cache");
		String[] cookies = getUsernameSetCookie(tokens, con);
		if (cookies.length == 0)
			return new BasicHttpResponse(_403);
		for (String cookie : cookies) {
			resp.addHeader("Set-Cookie", cookie);
		}
		return resp;
	}

	public String[] getUsernameSetCookie(Collection<String> tokens,
			ObjectConnection con) {
		String username = getUserLogin(tokens, con);
		if (username == null)
			return new String[0];
		int i = 0;
		String[] result = new String[userCookies.size()];
		for (String userCookie : userCookies) {
			String secure = userCookie.endsWith("s=") ? ";Secure" : "";
			result[i++] = userCookie + encode(username) + ";Path=" + protectedPath + secure;
		}
		return result;
	}

	public String getUserIdentifier(Collection<String> tokens,
			ObjectConnection con) throws OpenRDFException, IOException {
{'fraction_non_alphanumeric': 0.138902130468263, 'fraction_numerical': 0.010193978952000294, 'mean_word_length': 4.4576746047628575, 'pattern_counts': {'":': 10, '<': 43, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5997971', 'n_tokens_mistral': 9713, 'n_tokens_neox': 8628, 'n_words': 2074}