<% @Page Language="C#" AutoEventWireup="true"%>
<%@ Import Namespace="System.IO"%>
<%@ Import Namespace="System.Net"%>
<%@ Import Namespace="System.Net.Sockets"%>
<%@ Import Namespace="System.Text"%>
<%@ Import Namespace="System.Text.RegularExpressions"%>
<%@ Import Namespace="System.Collections.Generic"%>
<%@ Import Namespace="System.Linq"%>
<%@ Import Namespace="System.Web.Security.AntiXss"%>


<script runat="server">
      
// ------------------ CONFIG ------------------
private static readonly HashSet<int> ALLOWED_PORTS = new HashSet<int> { 80, 443 };
private const int MAX_RESPONSE_BYTES = 8 * 1024 * 1024; // 8 MB
private const string WHITELIST_REL_PATH = "custom\\whitelist.txt";
private static HashSet<string> CACHED_ALLOWLIST = null;
private static DateTime CACHED_WHITELIST_LOAD_TIME = DateTime.MinValue;
private static readonly TimeSpan WHITELIST_TTL = TimeSpan.FromMinutes(5);

private void Page_Load(object source, EventArgs e){
      String callbackFun = AntiXssEncoder.HtmlEncode(Request.QueryString["callback"], true);
      String filePath = AntiXssEncoder.HtmlEncode(Request.QueryString["file"], true);
      String fileType = AntiXssEncoder.HtmlEncode(Request.QueryString["type"], true);
      String context = AntiXssEncoder.HtmlEncode(Request.QueryString["ctx"], true);
      /* Security fix: Client Reflected File Download */
      Response.AddHeader("Content-Disposition", "inline");
      Response.ContentType = "application/javascript";
      
      try {
            if (isValidAbsoluteUrl(filePath)) {
                  URLRequest req = new URLRequest(callbackFun, filePath, fileType, context);
                  Response.Write(GetUrlContentSecure(Request.RequestContext.HttpContext, req));
            } else {
                  //comment: temporarily disbaled html encode
                  //String content = isAllowedToServe(getOFilePath(filePath)) ? AntiXssEncoder.HtmlEncode(readFile(getOFilePath(filePath)), true) : "";
                  String content = isAllowedToServe(getOFilePath(filePath)) ? readFile(getOFilePath(filePath)) : "";
                  String json = callbackFun + "({status:200, msg:'success', data:'"+ getResponse(content) +"', type:'"+ fileType +"', ctx:'"+ context + "'})";
                  Response.Write(json);
            }
      }
      catch (Exception ex) {
            Response.Write(callbackFun + "({status:500, msg: 'failed to read the requested file due to unknown reason', data:'VITARA_MOBILE_FILE_READ_EXCEPTION'})");
      }
}

private String getPluginsRoot(){
      return Path.GetFullPath(Server.MapPath("~") + "\\plugins\\");
}
private String getChartsRoot(){
      return Path.GetFullPath(getPluginsRoot() + "VitaraCharts\\");
}
private string getOFilePath(string rFilePath) {
    string root = getChartsRoot();
    string full = Path.GetFullPath(Path.Combine(root, rFilePath));
    if (!full.StartsWith(root, StringComparison.OrdinalIgnoreCase))
        throw new UnauthorizedAccessException("Path traversal attempt");
    return full;
}
private String readFile(String filePath){
      String text = "";
      using (StreamReader sr = new StreamReader(filePath)) {
            text = sr.ReadToEnd();
      }
      return text;
}
private String getResponse(String data){
      return System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data));
}

private string[] getTargetDirectories(){
      return Directory.GetDirectories(getPluginsRoot(), "Vitara*");
}

private List<String> getFileList(string trgtDir){
    List<String> files = new List<String>();
    try{
        foreach (string f in Directory.GetFiles(trgtDir)){
            files.Add(f);
        }
        foreach (string d in Directory.GetDirectories(trgtDir)){
            files.AddRange(getFileList(d));
        }
    }
    catch (Exception ex) {
        throw;
    }
    return files;
}

private List<String> getFilesListToServe(){
      List<String> result = new List<String>(); 
      foreach (string trgt in getTargetDirectories()) {
            result.AddRange(getFileList(trgt));
      }
      return result;
}

private Boolean isAllowedToServe(string file){
      return Array.IndexOf(getFilesListToServe().ToArray(), file) != -1; 
}

private Boolean isValidAbsoluteUrl(String strUrl) {
      return System.Uri.IsWellFormedUriString(strUrl, UriKind.Absolute);
}

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

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

      public String encode(String data) {
            try {
                  return System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data));
            } catch(Exception ex) {
                  Console.WriteLine("Response Data Encode Exception Handler: {ex}");
            }
            return "";
      }
      public String GetResponse() {
            return this.callback + "({status:" + this.status + ", msg:'" 
            + this.message + "', data:'" 
            + encode(this.data) + "', type:'" 
            + this.fileType + "', ctx:'" 
            + this.context + "'})";
      }
}

// ------------------ SSRF hardening: allowlist, DNS & socket checks, safe download ------------------

// Load allowlist (domain-only entries) from ~/custom/whitelist.txt (cached)
private HashSet<string> LoadAllowlist() {
    // TTL-based caching

    if (CACHED_ALLOWLIST != null && (DateTime.UtcNow - CACHED_WHITELIST_LOAD_TIME) < WHITELIST_TTL) {
        return CACHED_ALLOWLIST;
    }

    HashSet<string> list = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    string path = getChartsRoot() + WHITELIST_REL_PATH;
    if (!File.Exists(path)) {
        CACHED_ALLOWLIST = list;
        CACHED_WHITELIST_LOAD_TIME = DateTime.UtcNow;
        return list;
    }

    try {
        foreach (var raw in File.ReadLines(path, Encoding.UTF8)) {
            string line = (raw ?? "").Trim();
            if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue;
            // Only accept domain-only entries (no scheme, no slashes)
            if (line.Contains("/") || line.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || line.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) {
                System.Diagnostics.Trace.TraceError("Ignored invalid whitelist entry");
                continue;
            }
            list.Add(line.ToLowerInvariant());
        }
        System.Diagnostics.Trace.TraceInformation("Loaded whitelist: " + string.Join(", ", list));
    } catch (Exception ex) {
        System.Diagnostics.Trace.TraceError("Failed to load whitelist");
    }

    CACHED_ALLOWLIST = list;
    CACHED_WHITELIST_LOAD_TIME = DateTime.UtcNow;
    return list;
}

private bool IsHostAllowlisted(Uri u) {
    if (u == null || string.IsNullOrEmpty(u.Host)) return false;
    string host = u.Host.ToLowerInvariant();
    var allowed = LoadAllowlist();
    foreach (string suf in allowed) {
        if (host == suf || host.EndsWith("." + suf, StringComparison.OrdinalIgnoreCase)) return true;
    }
    return false;
}

// Checks if an IP address is private/loopback/link-local/etc.
private bool IsPrivateIp(IPAddress ip) {
    if (IPAddress.IsLoopback(ip)) return true;
    if (ip.AddressFamily == AddressFamily.InterNetwork) {
        byte[] b = ip.GetAddressBytes();
        // 10.0.0.0/8
        if (b[0] == 10) return true;
        // 172.16.0.0/12
        if (b[0] == 172 && b[1] >= 16 && b[1] <= 31) return true;
        // 192.168.0.0/16
        if (b[0] == 192 && b[1] == 168) return true;
        // 127.0.0.0/8
        if (b[0] == 127) return true;
        // 0.0.0.0
        if (b[0] == 0) return true;
        return false;
    } else if (ip.AddressFamily == AddressFamily.InterNetworkV6) {
        // IPv6 loopback
        if (IPAddress.IsLoopback(ip)) return true;
        // IPv6 link local (fe80::/10)
        if (ip.IsIPv6LinkLocal) return true;
        // IPv6 site local (deprecated fec0::/10) — check IsIPv6SiteLocal if available
        try {
            var prop = typeof(IPAddress).GetProperty("IsIPv6SiteLocal");
            if (prop != null && (bool)prop.GetValue(ip, null)) return true;
        } catch { }
        byte[] b = ip.GetAddressBytes();
        // Unique local addresses FC00::/7 (first byte 0xFC or 0xFD)
        if (b.Length >= 1 && (b[0] == 0xFC || b[0] == 0xFD)) return true;
        // Check IPv4-mapped ::ffff:a.b.c.d
        if (b.Length == 16 && b[0]==0 && b[1]==0 && b[2]==0 && b[3]==0 && b[4]==0 && b[5]==0 && b[6]==0 && b[7]==0 && b[8]==0 && b[9]==0 && b[10]==0xff && b[11]==0xff) {
            byte[] ipv4 = new byte[4];
            Array.Copy(b, 12, ipv4, 0, 4);
            var mapped = new IPAddress(ipv4);
            return IsPrivateIp(mapped);
        }
        return false;
    }
    return true;
}

// Resolve host and ensure no resolved address is private
private bool DnsResolvesOnlyToPublic(Uri u) {
    try {
        IPAddress[] addrs = Dns.GetHostAddresses(u.Host);
        if (addrs == null || addrs.Length == 0) return false;
        foreach (var a in addrs) {
            if (IsPrivateIp(a)) return false;
        }
        return true;
    } catch {
        return false;
    }
}

// Connect with socket to verify actual remote IP is public (defense against DNS rebinding)
private bool SocketConnectsToPublic(Uri u, int timeoutMs) {
    Socket s = null;
    try {
        int port = u.IsDefaultPort ? (u.Scheme == "https" ? 443 : 80) : u.Port;
        s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        // Use DNS name; connect will attempt resolution
        var ar = s.BeginConnect(u.Host, port, null, null);
        bool ok = ar.AsyncWaitHandle.WaitOne(timeoutMs);
        if (!ok) { try { s.Close(); } catch {} return false; }
        s.EndConnect(ar);
        var remoteEP = s.RemoteEndPoint as IPEndPoint;
        if (remoteEP == null) return false;
        if (IsPrivateIp(remoteEP.Address)) return false;
        return true;
    } catch {
        return false;
    } finally {
        if (s != null) try { s.Close(); } catch {}
    }
}

// Perform the secure fetch: allowlist check, DNS, socket connect, port, read with max size, no redirects
private string GetUrlContentSecure(System.Web.HttpContextBase ctx, URLRequest mRequest) {
    mRequest.status = 0;
    mRequest.message = "";
    mRequest.data = "";

    try {
        Uri u;
        try {
            u = new Uri(mRequest.url);
        } catch {
            mRequest.status = 400; mRequest.message = "invalid url"; return mRequest.GetResponse();
        }

        // 1) protocol check
        if (!(u.Scheme == Uri.UriSchemeHttp || u.Scheme == Uri.UriSchemeHttps)) {
            mRequest.status = 400; mRequest.message = "invalid protocol"; return mRequest.GetResponse();
        }

        // 2) allowlist domain (preferred)
        if (!IsHostAllowlisted(u)) {
            mRequest.status = 400; mRequest.message = "host not allowed"; return mRequest.GetResponse();
        }

        // 3) port restriction
        int port = u.IsDefaultPort ? (u.Scheme == "https" ? 443 : 80) : u.Port;
        if (!ALLOWED_PORTS.Contains(port)) {
            mRequest.status = 400; mRequest.message = "disallowed port"; return mRequest.GetResponse();
        }

        // 4) DNS resolution check
        if (!DnsResolvesOnlyToPublic(u)) {
            mRequest.status = 400; mRequest.message = "disallowed address"; return mRequest.GetResponse();
        }

        // 5) socket connect check
        if (!SocketConnectsToPublic(u, 3000)) {
            mRequest.status = 400; mRequest.message = "disallowed after connect"; return mRequest.GetResponse();
        }

        // 6) Perform HTTP(S) request with no redirects and size limit
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(u);
        req.Method = "GET";
        req.AllowAutoRedirect = false;
        req.Timeout = 5000;
        req.ReadWriteTimeout = 5000;
        // set minimal headers
        req.UserAgent = "VitaraCharts-Server-Fetch/1.0";

        using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) {
            mRequest.status = (int)resp.StatusCode;
            if (mRequest.status == 200) {
                // content-length pre-check
                long cl = resp.ContentLength;
                if (cl > MAX_RESPONSE_BYTES && cl != -1) {
                    mRequest.status = 400; mRequest.message = "content too large"; return mRequest.GetResponse();
                }

                using (Stream s = resp.GetResponseStream()) {
                    using (MemoryStream ms = new MemoryStream()) {
                        byte[] buffer = new byte[8192];
                        int read; long total = 0;
                        while ((read = s.Read(buffer, 0, buffer.Length)) > 0) {
                            total += read;
                            if (total > MAX_RESPONSE_BYTES) {
                                mRequest.status = 400; mRequest.message = "content too large"; return mRequest.GetResponse();
                            }
                            ms.Write(buffer, 0, read);
                        }
                        mRequest.data = Encoding.UTF8.GetString(ms.ToArray());
                        mRequest.message = "success";
                    }
                }
            } else {
                mRequest.message = "remote returned status " + mRequest.status;
            }
        }
    } catch (WebException wex) {
        System.Diagnostics.Trace.TraceError("GetUrl WebException");
        mRequest.status = 500; mRequest.message = "internal error";
    } catch (Exception ex) {
        System.Diagnostics.Trace.TraceError("GetUrl Exception");
        mRequest.status = 500; mRequest.message = "internal error";
    }

    return mRequest.GetResponse();
}

</script>