<%@ page import="java.io.*"%>
<%@ page import="java.net.*"%>
<%@ page import="java.util.*"%>
<%@ page import="java.util.regex.*"%>
<%@ page import="java.nio.charset.StandardCharsets"%>
<%@ page import="org.owasp.encoder.Encode"%>
<%@ page import="org.apache.commons.codec.binary.Base64"%>
<%@page contentType="application/json; charset=UTF-8"%>
<%!

String getCustomWhitelistPath(ServletContext ctx) {
    return getChartsRoot(ctx) + "custom/whitelist.txt";
}

Set<String> loadWhitelist(ServletContext ctx) {
    Set<String> list = new HashSet<>();
    File wlFile = new File(getCustomWhitelistPath(ctx));

    if (!wlFile.exists()) {
        System.err.println("[WARN] Whitelist file not found: " + wlFile.getAbsolutePath());
        return list;
    }

    try (BufferedReader reader = new BufferedReader(new FileReader(wlFile))) {
        String line;
        while ((line = reader.readLine()) != null) {
            line = line.trim();

            // Skip empty or comment lines
            if (line.isEmpty() || line.startsWith("#")) continue;

            // Validation: domain should not contain scheme or slashes
            if (line.contains("/") || line.startsWith("http://") || line.startsWith("https://")) {
                System.err.println("[WARN] Invalid whitelist entry ignored: " + line);
                continue;
            }

            // Normalize to lowercase
            list.add(line.toLowerCase(Locale.ROOT));
        }

        System.out.println("[INFO] Loaded whitelist entries: " + list);
    } catch (IOException e) {
        System.err.println("[ERROR] Failed to load whitelist: " + e.getMessage());
    }

    return list;
}


// Cache whitelist so it’s not re-read for every request.
Set<String> CACHED_ALLOWLIST = null;
long CACHED_WHITELIST_LOAD_TIME = 0L;
long WHITELIST_TTL_MS = 5 * 60 * 1000; // 5 minutes — reload if older than this

Set<String> getAllowlist(ServletContext ctx) {
    long now = System.currentTimeMillis();
    if (CACHED_ALLOWLIST == null || (now - CACHED_WHITELIST_LOAD_TIME > WHITELIST_TTL_MS)) {
        CACHED_ALLOWLIST = loadWhitelist(ctx);
        CACHED_WHITELIST_LOAD_TIME = now;
    }
    return CACHED_ALLOWLIST;
}

boolean isAllowedHostBySuffix(URL u, ServletContext ctx) {
    if (u == null) return false;
    String host = u.getHost();
    if (host == null || host.isEmpty()) return false;
    host = host.toLowerCase(Locale.ROOT);

    Set<String> ALLOWED_HOST_SUFFIXES = getAllowlist(ctx);
    for (String suf : ALLOWED_HOST_SUFFIXES) {
        if (host.equals(suf) || host.endsWith("." + suf)) {
            return true;
        }
    }
    return false;
}

private static final Set<Integer> ALLOWED_PORTS = Collections.unmodifiableSet(
    new HashSet<Integer>(Arrays.asList(80, 443))
);

private static final int MAX_RESPONSE_BYTES = 8 * 1024 * 1024; // 8MB limit

boolean isPrivateInetAddress(InetAddress addr) {
    return addr.isAnyLocalAddress() || addr.isLoopbackAddress() ||
           addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
}

boolean dnsResolvesToOnlyPublic(URL u) {
    try {
        InetAddress[] addrs = InetAddress.getAllByName(u.getHost());
        for (InetAddress a : addrs) {
            if (isPrivateInetAddress(a)) {
                return false;
            }
        }
        return true;
    } catch (UnknownHostException ex) {
        return false;
    }
}

boolean socketConnectsToPublic(URL u, int connectTimeoutMs) {
    Socket s = null;
    try {
        int port = (u.getPort() == -1) ? ("https".equalsIgnoreCase(u.getProtocol()) ? 443 : 80) : u.getPort();
        s = new Socket();
        s.connect(new InetSocketAddress(u.getHost(), port), connectTimeoutMs);
        InetAddress remote = s.getInetAddress();
        if (isPrivateInetAddress(remote)) {
            return false;
        }
        return true;
    } catch (Exception ex) {
        return false;
    } finally {
        if (s != null) {
            try { s.close(); } catch (IOException ignore) {}
        }
    }
}

String fetchUrlSecurely(ServletContext ctx, URLRequest mRequest) {
    HttpURLConnection conn = null;
    InputStream in = null;
    try {
        if (mRequest.url == null) {
            mRequest.status = 400;
            mRequest.message = "missing url";
            return mRequest.getResponse();
        }

        URL u;
        try {
            u = new URL(mRequest.url);
        } catch (MalformedURLException ex) {
            mRequest.status = 400;
            mRequest.message = "invalid url";
            return mRequest.getResponse();
        }

        String proto = u.getProtocol();
        if (!"http".equalsIgnoreCase(proto) && !"https".equalsIgnoreCase(proto)) {
            mRequest.status = 400;
            mRequest.message = "invalid protocol";
            return mRequest.getResponse();
        }

        // External whitelist check
        if (!isAllowedHostBySuffix(u, ctx)) {
            mRequest.status = 400;
            mRequest.message = "host not allowed";
            return mRequest.getResponse();
        }

        int port = (u.getPort() == -1) ? ("https".equalsIgnoreCase(proto) ? 443 : 80) : u.getPort();
        if (!ALLOWED_PORTS.contains(port)) {
            mRequest.status = 400;
            mRequest.message = "disallowed port";
            return mRequest.getResponse();
        }

        if (!dnsResolvesToOnlyPublic(u)) {
            mRequest.status = 400;
            mRequest.message = "disallowed address";
            return mRequest.getResponse();
        }

        if (!socketConnectsToPublic(u, 3000)) {
            mRequest.status = 400;
            mRequest.message = "disallowed after connect";
            return mRequest.getResponse();
        }

        conn = (HttpURLConnection) u.openConnection();
        conn.setInstanceFollowRedirects(false);
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(3000);
        conn.setReadTimeout(5000);

        int cl = conn.getContentLength();
        if (cl > MAX_RESPONSE_BYTES) {
            mRequest.status = 400;
            mRequest.message = "content too large";
            return mRequest.getResponse();
        }

        mRequest.status = conn.getResponseCode();
        if (mRequest.status == HttpURLConnection.HTTP_OK) {
            in = conn.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[8192];
            int r, total = 0;
            while ((r = in.read(buf)) != -1) {
                total += r;
                if (total > MAX_RESPONSE_BYTES) {
                    mRequest.status = 400;
                    mRequest.message = "too large";
                    return mRequest.getResponse();
                }
                baos.write(buf, 0, r);
            }
            mRequest.data = new String(baos.toByteArray(), StandardCharsets.UTF_8);
            mRequest.message = "success";
        } else {
            mRequest.message = "remote code " + mRequest.status;
        }
    } catch (Exception ex) {
        System.err.println("fetchUrlSecurely exception: " + ex.getMessage());
        mRequest.status = 500;
        mRequest.message = "internal error";
    } finally {
        try { if (in != null) in.close(); } catch (IOException ignore) {}
        try { if (conn != null) conn.disconnect(); } catch (Exception ignore) {}
    }
    return mRequest.getResponse();
}

String getPluginsRoot(ServletContext ctx){
      return ctx.getRealPath("/") + "/plugins/";
}

String getChartsRoot(ServletContext ctx){
      return getPluginsRoot(ctx) + "VitaraCharts/";
}

String getRFilePath(String rFilePath){
      return "/plugins/VitaraCharts/" + rFilePath;
}

void createFile(String path, String data) throws IOException{
      FileWriter filewriter = new FileWriter(path, true);
      filewriter.write(data);
      filewriter.close();
}

void deleteFile(String path){
      File mFile = new File(path);
      if (mFile.exists()){
            mFile.delete();
      }
}

String getOFilePath(ServletContext ctx, String rFilePath){
      return getChartsRoot(ctx) + rFilePath;
}

boolean isAllowedToServe(ServletContext ctx, String resourcePath) {
      try {
            URL resourceUrl = ctx.getResource(resourcePath);
            URL directoryUrl = ctx.getResource(getRFilePath(""));
        
            if (resourceUrl != null && directoryUrl != null) {
                String resourceUrlString = resourceUrl.toString();
                String directoryUrlString = directoryUrl.toString();
                return resourceUrlString.startsWith(directoryUrlString);
            }
      } catch (MalformedURLException ex) {

      }

    return false;
}

String readFileIS(ServletContext ctx, URLRequest mURLRequest) throws FileNotFoundException, IOException{
      InputStream is = ctx.getResourceAsStream(getRFilePath(mURLRequest.url));
      if (is != null) {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder sb = new StringBuilder();
            String line;
            while((line = reader.readLine())!= null){
                  sb.append(line+"\n");
            }
            reader.close();
            mURLRequest.status = 200;
            mURLRequest.message = "success";
            mURLRequest.data = sb.toString();
      }
      return mURLRequest.getResponse();
}

String readFile(ServletContext ctx, String rFilePath) throws FileNotFoundException, IOException{
      String path = getOFilePath(ctx, rFilePath);
      BufferedReader reader = new BufferedReader(new FileReader(path));
      StringBuilder sb = new StringBuilder();
      String line;
      while((line = reader.readLine())!= null){
          sb.append(line+"\n");
      }
      reader.close();
      return sb.toString();
}

String readFromURL(String url) throws MalformedURLException, IOException {
      URL mUrl = new URL(url);
      BufferedReader reader = new BufferedReader(new InputStreamReader(mUrl.openStream()));
      StringBuilder sb = new StringBuilder();
      String line;
      while((line = reader.readLine()) != null) {
            sb.append(line+"\n");
      }
      reader.close();
      return sb.toString();
}

String getResponse(String data){
      return new String(Base64.encodeBase64(data.getBytes()));
}

private class URLRequest {
      int status;
      String callback, url, message, data, fileType, context;

      URLRequest(String callback, String url, String fileType, String context) {
            this.callback = htmlEncode(callback);
            this.url = htmlEncode(url);
            this.fileType = htmlEncode(fileType);
            this.context = htmlEncode(context);
      }

      String htmlEncode(String data) {
            return Encode.forHtml(data);
      }

      String encode(String data) {
            try {
                  return new String(Base64.encodeBase64(data.getBytes()));
            } catch(Exception ex) {
                  ex.printStackTrace();
            }
            return "";
      }

      String getResponse() {
            return this.callback + "({status:" + this.status + ", msg:'" 
            + this.message + "', data:'" 
            + encode(htmlEncode(this.data)) + "', type:'" 
            + this.fileType + "', ctx:'" 
            + this.context + "'})";
      }

      void println(JspWriter out, String data) throws IOException {
            out.println(data);
      }

      void printError(JspWriter out, int status, String msg) throws IOException {
            this.status = status;
            this.message = msg;
            this.data = "VITARA_MOBILE_FILE_READ_EXCEPTION";
            this.println(out, this.getResponse());
      }
}

Boolean isValidAbsoluteUrl(String strUrl) {
    try {
        new URL(strUrl);
        return true;
    } catch (Exception e) {
        return false;
    }
}

%>

<%
      response.setContentType("application/javascript");
      String filePath = request.getParameter("file");
      String callbackFun = request.getParameter("callback");
      String fileType = request.getParameter("type");
      String context = request.getParameter("ctx");

      URLRequest mURLRequest = new URLRequest(callbackFun, filePath, fileType, context);

      try {
            if (isValidAbsoluteUrl(filePath)) {
                  mURLRequest.println(out, fetchUrlSecurely(application, mURLRequest));
            } else if (isAllowedToServe(application, getRFilePath(mURLRequest.url))) {
                  mURLRequest.println(out, readFileIS(application, mURLRequest));
            } else {
                  mURLRequest.printError(out, 400, "Invalid file path");
            }
      }
      catch(Exception e){
            mURLRequest.printError(out, 500, "Exception occured");
      }
%>
