
// SPDX-License-Identifier: CC-BY-NC-SA-4.0
//
// Copyright (C) 2026 Bit by Bit Signal Processing LLC (https://bxbsp.com)
//
// This work is placed under the "Creative Commons Attribution
// NonCommercial ShareAlike 4.0 International" license, known
// by the shortened acronym "CC-BY-NC-SA-4.0".
//
// This work is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// A CC-BY-NC-SA-4.0 license allows you to use this work for
// noncommercial purposes so long as attribution is made to the
// original author.  Modified versions of this work may be distributed,
// but only under the same license.  For further details, see the
// Creative Commons License "CC-BY-NC-SA-4.0".
//
// You should have received a copy of the CC-BY-NC-SA-4.0 license
// along with this work. If not, see
// <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
//

#include <time.h>

#include "protocol_http.hh"


#define BYTES_PER_HTTP_WRITE 1000

struct web_header
{
  const char* header;
  const char* value;
};


//
// From defauls with high security
//
//content-security-policy: default-src 'none'; img-src 'self' data: ; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'none';form-action 'self';
//referrer-policy: no-referrer
//x-content-type-options: nosniff
//x-frame-options: deny
//x-xss-protection: 1; mode=block
		       
web_header headers[] =
  {
   { "content-security-policy:", "default-src 'none'; img-src 'self' data: ; manifest-src 'self'; script-src 'self'; connect-src 'self' ws: wss: ; frame-ancestors 'none'; base-uri 'none';" },
   { "referrer-policy:",         "no-referrer" },
   { "x-content-type-options:",  "nosniff" },
   { "x-frame-options:",         "deny" },
   { "x-xss-protection:",        "1; mode=block" },
   };
const int num_headers = sizeof(headers) / sizeof(headers[0]);  


bool serve_file_from_list(struct lws* wsi, const char* requested_url, struct http_progress* progress)
{
  int error;
  unsigned char  buf      [LWS_PRE + BYTES_PER_HTTP_WRITE];
  unsigned char* start  = &buf[LWS_PRE];
  unsigned char* p      = start;
  unsigned char* end    = &buf[sizeof(buf) - 1];


  //
  // generate_204 is used to determine whether cellphones are connected to the internet.  We want to fake that
  // for when we're creating our own WiFi, so the cell phones don't try to reconnect to something else or to
  // disconnect.
  //
  if(!strcmp(requested_url, "generate_204"))
    {
      //error = lws_add_http_header_status(wsi, HTTP_STATUS_NO_CONTENT | LWSAHH_FLAG_NO_SERVER_NAME, &p, end);
      //if(error) return true;

      error = lws_add_http_header_by_name(wsi, (const unsigned char*)"HTTP/1.1", (const unsigned char*)"204 No Content", strlen("204 No Content"), &p, end);
      if(error) return true;

      error = lws_add_http_header_by_name(wsi, (const unsigned char*)"Content-Length:", (const unsigned char*)"0", strlen("0"), &p, end);
      if(error) return true;

      time_t time_struct;
      struct tm utc_time;
      time(&time_struct);
      gmtime_r(&time_struct, &utc_time);
      char timestring[100];
      strftime(timestring, 100, "%a, %d %b %Y %T GMT", &utc_time);
      
      error = lws_add_http_header_by_name(wsi, (const unsigned char*)"Date:", (const unsigned char*)timestring, strlen(timestring), &p, end);
      if(error) return true;

      error = lws_finalize_write_http_header(wsi, start, &p, end);
      if(error) return true;

      progress->wfile = 0;
      progress->fp = 0;
      progress->bytes_sent = 0;

      lws_callback_on_writable(wsi);  // Defer writing the main content until the LWS_CALLBACK_HTTP_WRITEABLE call.

      return false;
    }
	
  //
  // See if the file is in a list in memory
  //
  for(int i=0; i<num_wfiles; i++)
    if(!strcmp(requested_url, wfiles[i].path))
      {	
	const char* mime_type = lws_get_mimetype(wfiles[i].path, 0);
	
	if(!mime_type)
	  {
	    printf("ERROR:  Can't get mime type for file \"%s\".", requested_url);
	    return true;
	  }

	printf("Serving HTTP file \"%s\" with type \"%s\".\n", wfiles[i].path, mime_type);

	error = lws_add_http_common_headers(wsi, HTTP_STATUS_OK, mime_type, wfiles[i].length, &p, end);
	if(error) return true;

	for(int h=0; h<num_headers; h++)
	  {
	    error = lws_add_http_header_by_name(wsi, (const unsigned char*)headers[h].header, (const unsigned char*)headers[h].value, strlen(headers[h].value), &p, end);
	    if(error) return true;
	  }
	
	error = lws_finalize_write_http_header(wsi, start, &p, end);
	if(error) return true;
	
	progress->wfile = &wfiles[i];
	progress->fp = 0;
	progress->bytes_sent = 0;
	
	lws_callback_on_writable(wsi);  // Defer writing the main content until the LWS_CALLBACK_HTTP_WRITEABLE call.
	
	return false;
      }
  
  return true;
}


int callback_http(struct lws *               wsi,
		  enum lws_callback_reasons  reason,
		  void *                     user,
		  void *                     in,
		  size_t                     len)
{
  struct http_progress* progress = (struct http_progress*) user;

  unsigned char  buf      [LWS_PRE + BYTES_PER_HTTP_WRITE];
  unsigned char* start  = &buf[LWS_PRE];
  unsigned char* p      = start;
  unsigned char* end    = &buf[sizeof(buf) - 1];

  switch (reason)
    {
    case LWS_CALLBACK_CLIENT_WRITEABLE:
      //printf("connection established\n");
           
    case LWS_CALLBACK_HTTP:
      {	  	
	const char *requested_url = (char *) in;
	//printf("requested URI: %s\n", requested_url);
	
	if(requested_url[0]=='/')
	  requested_url++;
	
	if(requested_url[0]==0 || !strcmp(requested_url, "index.htm"))
	  requested_url="index.html";

	bool error = serve_file_from_list(wsi, requested_url, progress);

	if(!error)
	  return 0;
		
	//
	// See if the file is on disk, in /opt/BxB/Documents
	//
	char* buff = new char[len+2];
	buff[0] = '/';
	int pp, qq;
	
	for(pp=0,qq=1; pp<(int)len; pp++)
	  {
	    if(pp<int(len-2) && requested_url[pp]=='%' && requested_url[pp]=='2' && requested_url[pp]=='0')
	      {
		buff[qq] = ' ';
		qq++;
		pp += 3;
	      }
	    else if( (requested_url[pp]>='0' && requested_url[pp]<='9') ||
		(requested_url[pp]>='a' && requested_url[pp]<='z') ||
		(requested_url[pp]>='A' && requested_url[pp]<='Z') ||
		(requested_url[pp]=='/' && pp!=0 && requested_url[pp-1]!='/') ||
		(requested_url[pp]=='_') || (requested_url[pp]==' ') ||
		(requested_url[pp]=='.' && pp!=0 && requested_url[pp-1]!='.' && requested_url[pp-1]!='/') )
	      {
		buff[qq] = requested_url[pp];
		qq++;
	      }
	  }
	buff[qq] = 0;
	
	const char* prefix = "/opt/BxB/Documents/";
	for(pp=0; pp<(int)strlen(prefix); pp++)
	  {
	    if(buff[pp]!=prefix[pp])
	      {
		printf("Error for HTTP request of unhandled file \"%s\"!\n", requested_url);
		printf("   Requested file doesn't have the correct prefix \"%s\"\n", &prefix[1]);
		delete [] buff;
		bool error = serve_file_from_list(wsi, "index.html", progress);
		if(!error)
		  {
		    printf("Served \"index.html\" instead.\n");
		    return 0;
		  }
		return 1;
	      }
	  }

	FILE* fp = fopen(buff, "rb");
	if(!fp)
	  {
	    printf("Error for HTTP request of unhandled file \"%s\"!\n", requested_url);
	    printf("   Can't open file \"%s\".\n", buff);
	    delete [] buff;
	    bool error = serve_file_from_list(wsi, "index.html", progress);
	    if(!error)
	      {
		printf("Served \"index.html\" instead.\n");
		return 0;
	      }
	    return 1;
	  }

	progress->wfile = 0;
	progress->fp = fp;
	progress->bytes_sent = 0;
	fseek(fp, 0, SEEK_END);
	progress->fp_bytes_total = ftell(fp);

	const char* mime_type = lws_get_mimetype(buff, 0);

	if(!mime_type)
	  {
	    printf("ERROR:  Can't get mime type for file \"%s\".", &buff[1]);
	    delete [] buff;
	    return 1;
	  }

	printf("Serving HTTP file \"%s\" with type \"%s\" and length %ld.\n", &buff[1], mime_type, progress->fp_bytes_total);

	error = lws_add_http_common_headers(wsi, HTTP_STATUS_OK, mime_type, progress->fp_bytes_total, &p, end);
	if(error)
	  {
	    delete [] buff;
	    return 1;
	  }

	error = lws_finalize_write_http_header(wsi, start, &p, end);
	if(error)
	  {
	    delete [] buff;
	    return 1;
	  }
	
	delete [] buff;

	lws_callback_on_writable(wsi);  // Defer writing the main content until the LWS_CALLBACK_HTTP_WRITEABLE call.

        return 0;
      }
    case LWS_CALLBACK_HTTP_WRITEABLE:
      {
	//printf("http_writeable called to serve \"%s\", already served %d bytes of %d.\n", progress->wfile->path, progress->bytes_sent, progress->wfile->length);

	if(progress->wfile)
	  {
	    int length_to_serve = progress->wfile->length - progress->bytes_sent;

	    if(length_to_serve==0)
	      return -1;
	    
	    if(length_to_serve>BYTES_PER_HTTP_WRITE)
	      length_to_serve = BYTES_PER_HTTP_WRITE;
	    
	    for(int i=0; i<length_to_serve; i++)
	      start[i] = progress->wfile->data[i + progress->bytes_sent];
	    
	    lws_write_protocol type = (length_to_serve+progress->bytes_sent >= progress->wfile->length) ? LWS_WRITE_HTTP_FINAL : LWS_WRITE_HTTP;
	    
	    int length_served = lws_write(wsi, (unsigned char *)start, length_to_serve, type);
	    
	    if(length_served != length_to_serve)
	      return 1;
	    
	    progress->bytes_sent += length_to_serve;
	    
	    if(type==LWS_WRITE_HTTP_FINAL)
	      {
		//printf("Serving of file \"%s\" Done.\n", progress->wfile->path);
		if(lws_http_transaction_completed(wsi))
		  return -1;
	      }
	    
	    lws_callback_on_writable(wsi);
	  }
	else if(progress->fp)
	  {
	    int length_to_serve = progress->fp_bytes_total - progress->bytes_sent;

	    if(length_to_serve==0)
	      return -1;
	    
	    if(length_to_serve>BYTES_PER_HTTP_WRITE)
	      length_to_serve = BYTES_PER_HTTP_WRITE;

	    fseek(progress->fp, progress->bytes_sent, SEEK_SET);

	    long bytes = fread(start, 1, length_to_serve, progress->fp);
	    
	    if(bytes<=0)
	      {
		printf("Error reading bytes from HTTP file.\n");
		return -1;
	      }

	    lws_write_protocol type = (bytes+progress->bytes_sent >= progress->fp_bytes_total) ? LWS_WRITE_HTTP_FINAL : LWS_WRITE_HTTP;
	    
	    int length_served = lws_write(wsi, (unsigned char *)start, bytes, type);
	    
	    if(length_served != length_to_serve)
	      return 1;
	    
	    progress->bytes_sent += bytes;
	    
	    if(type==LWS_WRITE_HTTP_FINAL)
	      {
		//printf("Serving of file \"%s\" Done.\n", progress->wfile->path);
		if(lws_http_transaction_completed(wsi))
		  return -1;
	      }
	    
	    lws_callback_on_writable(wsi);
	  }
	else
	  {
	    // Only used for generate_204
	    lws_write_protocol type = LWS_WRITE_HTTP_FINAL;	    
	    int length_served = lws_write(wsi, (unsigned char *)start, 0, type);
	    if(length_served != 0)
	      return 1;
	  }
	
	return 0;
      }
    default:
      //printf("HTTP protocol:  unhandled callback reason %d (0x%08x)\n", reason, reason);
      break;
    }
   
  return 0;
}


const char* get_http_path_for_data(const unsigned char* data)
{
  for(int i=0; i<num_wfiles; i++)
    {
      if(data == wfiles[i].data)
	return wfiles[i].path;
    }
  
  return 0;
}

void add_http_file(const char* path, const unsigned char* data, int data_length)
{
  printf("Dynamically adding new web file \"%s\" to the web server.\n", path);
  
  if(num_wfiles==MAX_WFILES)
    {
      printf("ERROR: Already serving the maximum number of web files in add_http_file().\n");
      exit(20);
    }

  for(int i=0; i<num_wfiles; i++)
    {
      if(!strcmp(path, wfiles[i].path))
	{
	  // Already have path in the list
	  if(data == wfiles[i].data)
	    {
	      // Same data.  We're adding it twice.  Just return.
	      return;
	    }
	  else
	    {
	      // Different data.  An error.
	      printf("ERROR: Already serving path \"%s\".  Can't serve it again with different data.\n", path);
	      exit(20);
	    }
	}
    }

  wfiles[num_wfiles].path = path;
  wfiles[num_wfiles].data = data;
  wfiles[num_wfiles].length = data_length;
  num_wfiles++;
}
