Search code examples
c++httpsocketstcphttp-headers

Why does only Firefox display the response from this HTTP server?


I'm trying to get straight in my head the relationship between HTTP and TCP.

I tried to resolve (what I perceived as) contradictory answers from a web search of "tcp vs http" by writing a server that listens at a TCP socket bound to some address+port, then typing that address+port into a web brower.

Having done so, I saw that the content received at the accept()ed socket was text with human-readable "HTTP stuff" (my knowledge of HTTP isn't enough to intelligently identify the content).

From Chrome, my server receives:

GET / HTTP/1.1
Host: 127.0.0.23:9018
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

...and from Firefox, my server receives:

GET / HTTP/1.1
Host: 127.0.0.23:9018
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1

From the above results, I conjectured that HTTP is sending HTTP-conformant bytes (is it always ASCII?) over a TCP connection to a server's socket that has been accept()ed after listen()ing to a specific address+port.

So I further conjectured that in order to get content to show up in a web browser that connects to the address+port that my server is listen()ing at, my server should write() some kind of HTTP-compliant response to the socket.

This Stack Overflow Q&A gave me a candidate minimal HTTP response.

Putting it all together, my server's MCVE code is:

#include <arpa/inet.h>                                                                         
#include <cerrno>                                                                              
#include <cstdio>                                                                              
#include <cstring>                                                                             
#include <fcntl.h>                                                                             
#include <iostream>                                                                            
#include <netinet/in.h>                                                                        
#include <pthread.h>                                                                           
#include <semaphore.h>                                                                         
#include <stdexcept>                                                                           
#include <sstream>                                                                             
#include <sys/types.h>                                                                         
#include <sys/socket.h>                                                                        
#include <unistd.h>                                                                            
                                                                                               
#define IP "127.0.0.23"                                                                        
#define PORT (9018)                                                                            
                                                                                               
/**                                                                                            
 * A primitive, POC-level HTTP server that accepts its first incoming connection               
 * and sends back a minimal HTTP OK response.                                                  
 */                                                                                            
class Server {                                                                                 
private:                                                                                       
  static const std::string ip_;                                                                
  static const std::uint16_t port_{PORT};                                                      
  int listen_sock_;                                                                            
  pthread_t tid_;                                                                              
                                                                                               
public:                                                                                        
  /**                                                                                          
   * Ctor: create and bind listen_sock_ and start a thread for startRoutine().                 
   */                                                                                          
  Server() {                                                                                   
    using namespace std;                                                                       
    int result;                                                                                
                                                                                               
    if (! createSocket()) { throw runtime_error("failed creating socket"); }                   
    if (! bindSocket()) { throw runtime_error("failed binding socket"); }                      
                                                                                               
    if ((result = pthread_create(&tid_, NULL, startRoutine, this))) {                          
      std::stringstream ss;                                                                    
                                                                                               
      ss << "pthread_create() error " << errno << "(" << result << ")";                        
      std::cerr << ss.str() << std::endl;                                                      
      throw runtime_error("failed spawning Server thread");                                    
    }                                                                                          
  }                                                                                            
                                                                                               
  /** Dtor: wait for the spawned thread and destroy listen_sock_. */                           
  ~Server() {                                                                                  
    pthread_join( tid_, NULL );                                                                
    destroySocket();                                                                           
  }                                                                                            
                                                                                               
private:                                                                                       
  /** Creates listen_sock_ as a stream socket. */                                              
  bool createSocket() {                                                                        
    listen_sock_ = socket(PF_INET, SOCK_STREAM, 0);                                            
                                                                                               
    if (listen_sock_ < 0) {                                                                    
      std::stringstream ss;                                                                    
                                                                                               
      ss << "socket() error " << errno << "(" << strerror(errno) << ")";                       
      std::cerr << ss.str() << std::endl;                                                      
    }                                                                                          
                                                                                               
    return (listen_sock_ >= 0);                                                                
  }                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                    [138/573]
  /** Shuts down and closes listen_sock_. */                                                   
  void destroySocket() {                                                                       
    if (listen_sock_ >= 0) {                                                                   
      shutdown(listen_sock_, SHUT_RDWR);                                                       
      close(listen_sock_);                                                                     
    }                                                                                          
  }                                                                                            

  /** Binds listen_sock_ to ip_ and port_. */                                                  
  bool bindSocket() {                                                                          
    int ret;                                                                                   
    sockaddr_in me;                                                                            
    me.sin_family = PF_INET;                                                                   
    me.sin_port = htons(port_);                                                                
    me.sin_addr.s_addr = inet_addr(ip_.c_str());
    int optval = 1;                                                                            

    setsockopt(listen_sock_, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);

    if ((ret = bind(listen_sock_, (sockaddr*)&me, sizeof me))) {
      std::stringstream ss;                                                                    

      ss << "bind() error " << errno << "(" << strerror(errno) << ")";
      std::cerr << ss.str() << std::endl;                                                      
    }                                                                                          

    return (! ret);                                                                            
  }                                                                                            

  /**                                                                                          
   * Accept a connection from listen_sock_.                                                    
   * Caller guarantees listen_sock_ has been listen()ed to already.
   * @param tv [in, out] How long to wait to accept a connection.
   * @return accepted socket; -1 on any error.                                                 
   */                                                                                          
  int acceptConnection(timeval& tv) {                                                          
    int sock = -1;                                                                             
    int ret;                                                                                   
    fd_set readfds;                                                                            
    sockaddr_in peer;                                                                          
    socklen_t addrlen = sizeof peer;                                                           

    FD_ZERO(&readfds);                                                                         
    FD_SET(listen_sock_, &readfds);                                                            
    ret = select(listen_sock_ + 1, &readfds, NULL, NULL, &tv);

    if (ret < 0) {                                                                             
      std::stringstream ss;                                                                    

      ss << "select() error " << errno << "(" << strerror(errno) << ")";
      std::cerr << ss.str() << std::endl;                                                      

      return sock;                                                                             
    }                                                                                          
    else if (! ret) {                                                                          
      std::cout << "no connections within " << tv.tv_sec << "seconds"
        << std::endl;                                                                          

      return sock;                                                                             
    }                                                                                          

    if ((sock = accept(listen_sock_, (sockaddr*)&peer, &addrlen)) < 0) {
      std::stringstream ss;                                                                    

      ss << "accept() error " << errno << "(" << strerror(errno) << ")";
      std::cerr << ss.str() << std::endl;                                                      
    }                                                                                          
    else {                                                                                     
      std::stringstream ss;                                                                    

      ss << "socket " << sock << " accepted connection from "
        << inet_ntoa( peer.sin_addr ) << ":" << ntohs(peer.sin_port);
      std::cout << ss.str() << std::endl;                                                      
    }                                                                                          

    return sock;                                                                               
  }
                                                                                                                                                                                                                                                                                                                                                                                     [60/573]
  /** Read from the specified socket and dump to stdout. */
  static void dumpReceivedContent(const int& sock) {
    fd_set readfds;                                                                            
    struct timeval tv = {30, 0};                                                               
    int ret;                                                                                   

    FD_ZERO(&readfds);                                                                         
    FD_SET(sock, &readfds);                                                                    
    ret = select(sock + 1, &readfds, NULL, NULL, &tv);

    if (ret < 0) {                                                                             
      std::stringstream ss;                                                                    

      ss << "select() error " << errno << "(" << strerror(errno) << ")";
      std::cerr << ss.str() << std::endl;                                                      

      return;                                                                                  
    }                                                                                          
    else if (! ret) {                                                                          
      std::cout << "no content received within " << tv.tv_sec << "seconds"
        << std::endl;                                                                          

      return;                                                                                  
    }                                                                                          

    if (FD_ISSET(sock, &readfds)) {                                                            
      ssize_t bytes_read;                                                                      
      char buf[80] = {0};                                                                      

      fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
      std::cout << "received content:" << std::endl;
      std::cout << "----" << std::endl;                                                        
      while ((bytes_read = read(sock, buf, (sizeof buf) - 1)) >= 0) {
        buf[bytes_read] = '\0';                                                                
        std::cout << buf;                                                                      
      }                                                                                        
      std::cout << std::endl << "----" << std::endl;
    }                                                                                          
  }                                                                                            

  /** Write a minimal HTTP OK response to the specified socker. */
  static void sendMinHttpResponse(const int& sock) {
    static const std::string resp =                                                            
      "HTTP/1.1 200 OK\r\n"                                                                    
      "Content-Length: 13\r\n"                                                                 
      "Content-Type: text/plain\r\n\r\nHello World!";
    write(sock, resp.c_str(), resp.length());                                                  
  }                                                                                            

  /**                                                                                          
   * Thread start routine: listen for, then accept connections; dump received
   * content; send a minimal response.                                                         
   */                                                                                          
  static void* startRoutine(void* arg) {                                                       
    Server* s;                                                                                 

    if (! (s = (Server*)arg)) {                                                                
      std::cout << "Bad arg" << std::endl;                                                     
      return NULL;                                                                             
    }                                                                                          

    if (listen(s->listen_sock_, 3)) {                                                          
      std::stringstream ss;                                                                    

      ss << "listen() error " << errno << "(" << strerror(errno) << ")";
      std::cerr << ss.str() << std::endl;                                                      

      return NULL;                                                                             
    }                                                                                          

    std::cout << "Server accepting connections at "
      << s->ip_ << ":" << s->port_ << std::endl;

    {                                                                                          
      timeval tv = { 30, 0 };                                                                  

      int sock = s->acceptConnection(tv);                                                      

      if (sock < 0) {                                                                          
        std::cout << "no connections accepted" << std::endl;

        return NULL;                                                                           
      }                                                                                        

      dumpReceivedContent(sock);                                                               
      sendMinHttpResponse(sock);                                                               
      shutdown(sock, SHUT_RDWR);                                                               
      close(sock);                                                                             
    }                                                                                          

    return NULL;                                                                               
  }                                                                                            
};                                                                                             

const std::string Server::ip_{IP};                                                             

int main( int argc, char* argv[] ) {                                                           
  Server s;                                                                                    
  return 0;                                                                                    
}

When I point Chrome and Chromium browsers to my server (127.0.0.23:9018), I get a blank page with no content, but when I point Firefox to my server, I get the "Hello world!" string that I wanted.

Why does this only work with Firefox, and not with Chrome or Chromium?


Solution

  • Your server responds with an invalid data size Content-Length: 13.

    1. The data is Hello World!, the size is 12.
    2. resp.length() does not count \0, thus the server does not send Hello World!\0.

    The header must be Content-Length: 12.