anchor.diff:      String[] allowedReferers;
   
       public ServerUrl[] getServerUrls() {
  -      return this.serverUrls;
  +       return this.serverUrls;
       }
       public void setServerUrls(ServerUrl[] value){
  -      this.serverUrls = value;
  +        this.serverUrls = value;
       }
   
       public boolean getMustMatch(){
  -      return this.mustMatch;
  +        return this.mustMatch;
       }
       public void setMustMatch(boolean value){
  -      this.mustMatch = value;
  +        this.mustMatch = value;
       }
   
       public String[] getAllowedReferers(){
  -      return this.allowedReferers;
  +        return this.allowedReferers;
       }
       public void setAllowedReferers(String[] value){
  -      this.allowedReferers = value;
  +        this.allowedReferers = value;
       }
   
       public String getLogFile(){
  -      return this.logFile;
  +        return this.logFile;
       }
       public void setLogFile(String value){
  -      this.logFile = value;
  +        this.logFile = value;
       }
   
       public String getLogLevel(){
  -      return this.logLevel;
  +        return this.logLevel;
       }
       public void setLogLevel(String value){
  -      this.logLevel = value;
  +        this.logLevel = value;
       }
   
       public ServerUrl getConfigServerUrl(String uri) {
  -      //split request URL to compare with allowed server URLs
  -      String[] uriParts = uri.split("(/)|(\\?)");
  -      String[] configUriParts = new String[] {};
  -
  -      for (ServerUrl su : serverUrls) {
  -        //if a relative path is specified in the proxy configuration file, append what's in the request itself
  -        if (!su.getUrl().startsWith("http"))
  -          su.setUrl(new StringBuilder(su.getUrl()).insert(0, uriParts[0]).toString());
  -
  -        configUriParts = su.getUrl().split("/");
  -
  -        //if the request has less parts than the config, don't allow
  -        if (configUriParts.length > uriParts.length) continue;
  -
  -        int i = 1;
  -        //skip comparing the protocol, so that either http or https is considered valid
  -        for (i = 1; i < configUriParts.length; i++)
  -        {
  -          if (!configUriParts[i].toLowerCase().equals(uriParts[i].toLowerCase()) ) break;
  -        }
  -        if (i == configUriParts.length)
  -        {
  -          //if the urls don't match exactly, and the individual matchAll tag is 'false', don't allow
  -          if (configUriParts.length == uriParts.length || su.getMatchAll())
  -            return su;
  -        }
  -      }
  -
  -      if (this.mustMatch)
  -        return null;//if nothing match and mustMatch is true, return null
  +        //split request URL to compare with allowed server URLs
  +        String[] uriParts = uri.split("(/)|(\\?)");
  +        String[] configUriParts = new String[] {};
  +
  +        for (ServerUrl su : serverUrls) {
  +            //if a relative path is specified in the proxy configuration file, append what's in the request itself
  +            if (!su.getUrl().startsWith("http"))
  +                su.setUrl(new StringBuilder(su.getUrl()).insert(0, uriParts[0]).toString());
  +
  +            configUriParts = su.getUrl().split("/");
  +
  +            //if the request has less parts than the config, don't allow
  +            if (configUriParts.length > uriParts.length) continue;
  +
  +            int i = 1;
  +            //skip comparing the protocol, so that either http or https is considered valid
  +            for (i = 1; i < configUriParts.length; i++)
  +            {
  +                if (!configUriParts[i].toLowerCase().equals(uriParts[i].toLowerCase()) ) break;
  +            }
  +            if (i == configUriParts.length)
  +            {
  +                //if the urls don't match exactly, and the individual matchAll tag is 'false', don't allow
  +                if (configUriParts.length == uriParts.length || su.getMatchAll())
  +                    return su;
  +            }
  +        }
  +
  +        if (this.mustMatch)
  +            return null;//if nothing match and mustMatch is true, return null
         else
  -        return new ServerUrl(uri); //if mustMatch is false send the server URL back that is the same the uri to pass thru
  +            return new ServerUrl(uri); //if mustMatch is false send the server URL back that is the same the uri to pass thru
       }
   
       public static boolean isUrlPrefixMatch(String prefix, String uri){
  -      return uri.toLowerCase().startsWith(prefix.toLowerCase()) ||
  -              uri.toLowerCase().replace("https://","http://").startsWith(prefix.toLowerCase()) ||
  -              uri.toLowerCase().substring(uri.indexOf("//")).startsWith(prefix.toLowerCase());
  -    }
  -  }
  -
  -  public static class ServerUrl {
  +        return uri.toLowerCase().startsWith(prefix.toLowerCase()) ||
  +                uri.toLowerCase().replace("https://","http://").startsWith(prefix.toLowerCase()) ||
  +                uri.toLowerCase().substring(uri.indexOf("//")).startsWith(prefix.toLowerCase());
  +    }
  +}
  +
  +public static class ServerUrl {
       String url;
       boolean matchAll;
       String oauth2Endpoint;
  
positive.code:     long _lastUpdate = new Date().getTime();
  
      public RateMeter(int rateLimit, int rateLimitPeriod){
        this._rate = (double) rateLimit / rateLimitPeriod / 60;
        this._countCap = rateLimit;
      }
  
      //called when rate-limited endpoint is invoked
      public boolean click() {
        long ts = (new Date().getTime() - _lastUpdate) / 1000;
        this._lastUpdate = new Date().getTime();
        //assuming uniform distribution of requests over time,
        //reducing the counter according to # of seconds passed
        //since last invocation
        this._count = Math.max(0, this._count - ts * this._rate);
        if (this._count <= this._countCap) {
          //good to proceed
          this._count++;
          return true;
        }
        return false;
      }
  
      public boolean canBeCleaned() {
        long ts = (new Date().getTime() - this._lastUpdate) / 1000;
        return this._count - ts * this._rate <= 0;
      }
  
    }
  
    private String PROXY_REFERER = "http://localhost/proxy.jsp";
    private static String DEFAULT_OAUTH = "https://www.arcgis.com/sharing/oauth2/";
    private static int CLEAN_RATEMAP_AFTER = 10000;
  
    //setReferer if real referer exist
    private void setReferer(String r){
      PROXY_REFERER = r;
    }
  
    private byte[] readRequestPostBody(HttpServletRequest request) throws IOException{
      int clength = request.getContentLength();
      if(clength > 0) {
        byte[] bytes = new byte[clength];
        DataInputStream dataIs = new DataInputStream(request.getInputStream());
  
        dataIs.readFully(bytes);
        dataIs.close();
        return bytes;
      }
  
      return new byte[0];
    }
  
    private HttpURLConnection forwardToServer(HttpServletRequest request, String uri, byte[] postBody) throws IOException{
      Enumeration headerNames = request.getHeaderNames();
      HashMap<String, String> mapHeaderInfo = new HashMap<String, String>();
  
      while (headerNames.hasMoreElements()) {
        String key = (String) headerNames.nextElement();
        String value = request.getHeader(key);
        if (!key.equalsIgnoreCase("host")) mapHeaderInfo.put(key, value);
      }
  
      return
              postBody.length > 0 ?
                      doHTTPRequest(uri, postBody, "POST", mapHeaderInfo) :
                      doHTTPRequest(uri, request.getMethod());
    }
  
    private boolean fetchAndPassBackToClient(HttpURLConnection con, HttpServletResponse clientResponse, boolean ignoreAuthenticationErrors) throws IOException{
      if (con!=null){
        Map<String, List<String>> headerFields = con.getHeaderFields();
  
        Set<String> headerFieldsSet = headerFields.keySet();
        Iterator<String> hearerFieldsIter = headerFieldsSet.iterator();
  
        while (hearerFieldsIter.hasNext()){
          String headerFieldKey = hearerFieldsIter.next();
          List<String> headerFieldValue = headerFields.get(headerFieldKey);
          StringBuilder sb = new StringBuilder();
          for (String value : headerFieldValue) {
            sb.append(value);
            sb.append("");
          }
          if (headerFieldKey != null) clientResponse.addHeader(headerFieldKey, sb.toString());
        }
  
        InputStream byteStream;
        if (con.getResponseCode() >= 400 && con.getErrorStream() != null){
          if (ignoreAuthenticationErrors && (con.getResponseCode() == 498 || con.getResponseCode() == 499)) return true;
          byteStream = con.getErrorStream();
        }else{
          byteStream = con.getInputStream();
        }
  
        clientResponse.setStatus(con.getResponseCode());
  
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        final int length = 5000;
  
        byte[] bytes = new byte[length];
        int bytesRead = 0;
  
        while ((bytesRead = byteStream.read(bytes, 0, length)) > 0) {
          buffer.write(bytes, 0, bytesRead);
        }
        buffer.flush();
  
        byte[] byteResponse = buffer.toByteArray();
        OutputStream ostream = clientResponse.getOutputStream();
        ostream.write(byteResponse);
        ostream.close();
        byteStream.close();
      }
      return false;
    }
  
    private boolean passHeadersInfo(Map mapHeaderInfo, HttpURLConnection con) {
  
      Iterator it = mapHeaderInfo.entrySet().iterator();
      while (it.hasNext()) {
        Map.Entry pair = (Map.Entry)it.next();
  
negative.code:     output.write(message.getBytes());
  
      output.flush();
  }
  
  private static void _sendURLMismatchError(HttpServletResponse response, String attemptedUri) throws IOException{
       sendErrorResponse(response, "Proxy has not been set up for this URL. Make sure there is a serverUrl in the configuration file that matches: " + attemptedUri,
                   "Proxy has not been set up for this URL.", HttpServletResponse.SC_FORBIDDEN);
  }
  
  private static void _sendPingMessage(HttpServletResponse response, String version, String config, String log) throws IOException{
      response.setStatus(HttpServletResponse.SC_OK);
      response.setHeader("Content-Type", "application/json");
      String message = "{ " +
          "\"Proxy Version\": \"" + version + "\"" +
          //", \"Java Version\": \"" + System.getProperty("java.version") + "\"" +
          ", \"Configuration File\": \"" + config + "\""  +
          ", \"Log File\": \"" + log + "\"" +
          "}";
      OutputStream output = response.getOutputStream();
      output.write(message.getBytes());
      output.flush();
  }
  %><%
  String uri = request.getQueryString();
  _log(Level.INFO, "Creating request for: " + uri);
  ServerUrl serverUrl;
  boolean passThrough = false;
  try {
      try {
  
          out.clear();
          out = pageContext.pushBody();
  
          if (uri == null || uri.isEmpty()){
              String errorMessage = "This proxy does not support empty parameters.";
              _log(Level.WARNING, errorMessage);
              sendErrorResponse(response, errorMessage, "400 - " + errorMessage, HttpServletResponse.SC_BAD_REQUEST);
              return;
          }
  
          if (uri.equalsIgnoreCase("ping")){
              String checkConfig = (getConfig().canReadProxyConfig() == true) ? "OK": "Not Readable";
              String checkLog = (okToLog() == true) ? "OK": "Not Exist/Readable";
              _sendPingMessage(response, version, checkConfig, checkLog);
              return;
          }
  
          //check if the uri is encoded then decode it
          if (uri.toLowerCase().startsWith("http%3a%2f%2f") || uri.toLowerCase().startsWith("https%3a%2f%2f")) uri= URLDecoder.decode(uri, "UTF-8");
  
          String[] allowedReferers = getConfig().getAllowedReferers();
          if (allowedReferers != null && allowedReferers.length > 0 && request.getHeader("referer") != null){
              setReferer(request.getHeader("referer")); //replace PROXY_REFERER with real proxy
              String hostReferer = request.getHeader("referer");
              try{
                  //only use the hostname of the referer url
                  hostReferer = new URL(request.getHeader("referer")).getHost();
              }catch(Exception e){
                  _log(Level.WARNING, "Proxy is being used from an invalid referer: " + request.getHeader("referer"));
                  sendErrorResponse(response, "Error verifying referer. ", "403 - Forbidden: Access is denied.", HttpServletResponse.SC_FORBIDDEN);
                  return;
              }
              if (!checkReferer(allowedReferers, hostReferer)){
                  _log(Level.WARNING, "Proxy is being used from an unknown referer: " + request.getHeader("referer"));
                  sendErrorResponse(response, "Unsupported referer. ", "403 - Forbidden: Access is denied.", HttpServletResponse.SC_FORBIDDEN);
                  return;
              }
          }
  
          //Check to see if allowed referer list is specified and reject if referer is null
          if (request.getHeader("referer") == null && allowedReferers != null && !allowedReferers[0].equals("*")){
          _log(Level.WARNING, "Proxy is being called by a null referer.  Access denied.");
                  sendErrorResponse(response, "Current proxy configuration settings do not allow requests which do not include a referer header.", "403 - Forbidden: Access is denied.", HttpServletResponse.SC_FORBIDDEN);
                  return;
      }
  
          serverUrl = getConfig().getConfigServerUrl(uri);
          if (serverUrl == null) {
              //if no serverUrl found, send error message and get out.
              _sendURLMismatchError(response, uri);
              return;
          }
          passThrough = serverUrl == null;
      } catch (IllegalStateException e) {
          _log(Level.WARNING, "Proxy is being used for an unsupported service: " + uri);
  
          _sendURLMismatchError(response, uri);
  
          return;
      }
  
      //Throttling: checking the rate limit coming from particular referrer
      if (!passThrough && serverUrl.getRateLimit() > -1) {
          synchronized(_rateMapLock){
              ConcurrentHashMap<String, RateMeter> ratemap = (ConcurrentHashMap<String, RateMeter>)application.getAttribute("rateMap");
              if (ratemap == null){
                  ratemap = new ConcurrentHashMap<String, RateMeter>();
                  application.setAttribute("rateMap", ratemap);
                  application.setAttribute("rateMap_cleanup_counter", 0);
              }
  
  
              String key = "[" + serverUrl.getUrl() + "]x[" + request.getRemoteAddr() + "]";
              RateMeter rate = ratemap.get(key);
              if (rate == null) {
                  rate = new RateMeter(serverUrl.getRateLimit(), serverUrl.getRateLimitPeriod());
                  RateMeter rateCheck = ratemap.putIfAbsent(key, rate);
                  if (rateCheck != null){
                      rate = rateCheck;
                  }
              }
              if (!rate.click()) {
                  _log(Level.WARNING, "Pair " + key + " is throttled to " + serverUrl.getRateLimit() + " requests per " + serverUrl.getRateLimitPeriod() + " minute(s). Come back later.");
  
                  sendErrorResponse(response, "This is a metered resource, number of requests have exceeded the rate limit interval.",
                          "Error 429 - Too Many Requests", 429);
  
                  return;
              }
  
              //making sure the rateMap gets periodically cleaned up so it does not grow uncontrollably
              int cnt = ((Integer)application.getAttribute("rateMap_cleanup_counter")).intValue();
              cnt++;
              if (cnt >= CLEAN_RATEMAP_AFTER) {
                  cnt = 0;
                  cleanUpRatemap(ratemap);
              }
              application.setAttribute("rateMap_cleanup_counter", new Integer(cnt));
          };
      }
  
      //readying body (if any) of POST request
  