HTTP ja HTTPS palvelimen pääohjelma

Kirjoitin uuden version terttu palvelimen pääohjelmasta. Nyt se pystyy vastaamaan myös https viesteihin. Kokeiluversiossa ohjelma antaa satunnaislukuja, ja sitä voi ajaa näistä linkeistä http://moijari.com:5001 ja https://moijari.com:5001.

En vielä ostanut certifikaattia moijari.com:ille, joten moijari.com nettiosoitteelle pitää luoda poikkeus, jos haluaa sitä käyttää https:llä.

Ohjelman alussa ovat c tyyliset includelauseet, joilla luodaan erilaisia muuttujia, rakenteita ja funktioiden mallikutsuja:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <openssl/ssl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>

/* See: http://h41379.www4.hpe.com/doc/83final/ba554_90007/ch04s03.html */

#include <netdb.h>

Aiemmissa tärkein on ehkä tuo <openssl/ssl.h>, joka käytännössä kuvaa nämä tämän ohjelman SSL alkuiset rutiinit. See kappaleessa on itseasiassa nettisivu, jota käytin apuna rakennuksessa.

#define DEFAULT_PORT "5001"
#define backlog 5

int s,news;
char *cert_file = "cert.pem";
char *privatekey_file = "key.pem";

char htmlin[32768], html1[32768], html2[32768], *html;

int usehttps=1;
int usehttpthruhttps=0;

Tässä tärkeimmät ehkä porttinumero, tiedostot, joista https avaimet ja sertifikaatti luetaan ja puskurit asiakkaan viestin lukuun ja kirjoitukseen (in=luku, 1 ja 2 kirjoitus).

Pääohjelman alussa määritellään pääohjelman tarvitsemia muuttujia, ja kutsutaan https liittymän “alustamiseen” liittyviä asioita.

void main(int argc,char *argv[])
{
  int c, listenfd, status, bytes, addr_size,
      len, callid;
  unsigned char timebuf[128];

  time_t now;

  SSL_METHOD *method=NULL;
  SSL_CTX *ctx=NULL;
  SSL *ssl;
  X509 *peer_cert;

  struct sockaddr sa_serv;
  struct sockaddr_in sa_cli;
  struct addrinfo hints;
  struct addrinfo *res;

  char buffer[32768],*p;
  callid=0;

  signal(SIGPIPE,SIG_IGN);

  procname=argv[0];
#ifdef OLD1
  if(usehttps) {
    myport=HTTPS_PORT;
  } else {
    myport=DEFAULT_PORT;
  }
#else
  myport=DEFAULT_PORT;

Alun kokonaislukumuuttujat (int) ovat perinteistä liittymää varten. Perinteisessä liittymässä http tiedon siirto tehdään socket, bind, listen, accept, read- ja write kutsujen avulla. https tiedon siirto tehdään SSL alkuisten kutsujen avulla. edelleen struct komennolla määritellyt muistialueet ovat perinteisiä funktioita varten. signal() funktiolla ohitetaan ilmeisesti writen katketessa lähetetty PIPe signaali. Procnameen talletetaan ohjelman nimi komentorivin ensimmäisestä sanasta. Ohjelman nimeä käytetään virheilmoituksissa. Oletusportti on aina DEFAULT_PORT, jonka arvo on tällä hetkellä 5001. Järjestelmäfunktioista saa kuvauksen komennolla $ man [komento], esimerkiksi $ man SSL_read tai $ man read.

Seuraavassa pätkässä ovat ensimmäiset SSL-rutiinit, näitä ajetaan vain yhden kerran ohjelman suorituksen alussa. usehttps flagillä määritellään käytetäänkö ohjelmassa HTTPS:ää. Eli tässä tapauksessa jos https on käytössä kutsutaan SSL-funktiot SSL_library_init(),  OpenSSL_add_ssl_algorithms(), SSL_load_error_strings(), SSLv23_server_method(), SSL_CTX_new(), SSL_CTX_use_certificate_file(), SSL_CTX_use_PrivateKey_file(), SSL_CTX_load_verify_locations.

   if(usehttps) {

    SSL_library_init();

    OpenSSL_add_ssl_algorithms();

    SSL_load_error_strings();

    if((method=(SSL_METHOD *)    
      SSLv23_server_method())==NULL) {
      fprintf(stderr,"\n%s: cannot SSLv3_server_method()", procname);
      fflush(stderr);
    }

    if((ctx=SSL_CTX_new(method))==NULL) {
      fprintf(stderr,"\n%s: cannot SSL_CTX_new()", procname);
      fflush(stderr);
    }

    if(SSL_CTX_use_certificate_file(ctx,
      cert_file, SSL_FILETYPE_PEM)<=0) {
      fprintf(stderr,"\n%s: cannot SSL_CTX_use_certificate()", procname);
      fflush(stderr);
    }

    if(SSL_CTX_use_PrivateKey_file(ctx,
      privatekey_file, SSL_FILETYPE_PEM)<=0) \
    {
      fprintf(stderr,"\n%s: cannot SSL_CTX_use_certificate()", procname);
      fflush(stderr);
    }

    if(!SSL_CTX_load_verify_locations(ctx, 
      cert_file, NULL)) {
      fprintf(stderr,"\n%s: cannot SSL_CTX_verify_locations()", procname);
      fflush(stderr);
    }
  }

Tössö ensimmäiset perinteiset kutsut: näitä kutsutaan kanssa vain kerran: getaddrinfo, socket(),  bind(),  listen().

  memset(&hints, 0, sizeof(hints);
  hints.ai_family = AF_UNSPEC;                   
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;

  if ((status = getaddrinfo(NULL, myport, &hints, &res)) != 0) {
    fprintf(stderr, "\n%s: getaddrinfo error: %s",
            procname,gai_strerror(status));
    fprintf(stderr, ", error code %d\n", status);
    fflush(stderr);
  }

  if((s = socket(res->ai_family, res->ai_socktype, res->ai_protocol))==-1) {
    fprintf(stderr, "%s: socket(), error: %d\n", procname, errno);
    perror("socket");
    fflush(stderr);
  }

  if(bind(s, res->ai_addr, res->ai_addrlen)==-1) {
    fprintf(stderr,"\n%s: cannot bind(), error: %d\n", procname, errno);
    perror("bind");
    fflush(stderr);
  }

  freeaddrinfo(res);

  if((listenfd=listen(s,backlog))==-1) {
    fprintf(stderr,"\n%s: cannot listen()\n", procname);
    perror("listen");
    fflush(stderr);
  }

Seuraava on ns pääluuppi, joka suoritetaan aina kun veppilomakkeelle kirjoitetaan tietoja ja painetaan nappulaa. Täytetyt tiedot ovat read (tai SSL_read kutsulla luetussa htmlin muuttujassa ja lähetettävä tieto kootaan htmlin:in perusteella html1 ja html 2 tauluihin. Lähetettävää tietoa varten on kaksi taulua, koska taulu 1 sisältää rivin, jossa on taulun 2 merkkien lukumäärä.

Jokaisella weppitapahtumalla käydään läpi accept(), SSL_new(), SSL_set_fd, SSL_accept(),

 for(;;) {
    usehttpthruhttps=0;
    addr_size=sizeof(sa_cli);
    news=accept(s, (struct sockaddr *)&sa_cli, &addr_size);

    fprintf(stdout,"\nConnection from %x, port %d",
            sa_cli.sin_addr.s_addr,
            sa_cli.sin_port);
    if(usehttps) {
      if((ssl=SSL_new(ctx))==NULL) {
        fprintf(stderr,"\n%s: cannot SSL_new()", procname);
        fflush(stderr);
      }

      if(SSL_set_fd(ssl,news)!=1) {
        fprintf(stderr,"\n%s: cannot SSL_set_fd()", procname);
        fflush(stderr);
      }

      if((status=SSL_accept(ssl))<0) {
        if(status==-1 && SSL_get_error(ssl,status)==1) {
          fprintf(stdout,"\nUsing http thru https");
          usehttpthruhttps=1;
        } else {
          fprintf(stderr,"\n%s: cannot SSL_accept(), status: %d, SSL error: %d",  procname, status, SSL_get_error(ssl,status));
          fflush(stderr);
        }
      }
    }
    if(usehttps && usehttpthruhttps==0) {

      peer_cert=SSL_get_peer_certificate(ssl);
      if(peer_cert==NULL) {
        fprintf(stdout,", No peer certificate");
        fflush(stdout);
      }
    }

Tässä on ajateltu, että jos SSL_accept kaatuu virheeseen 1, yhteys ei ole HTTPS yhteys vaan http yhteys. HTTP yhteyden merkiksi laitetaan muuttujaan usehttpthruhttps 1.

Sitten onkin readin vuoro.

   memset(htmlin,0,sizeof(htmlin));

   if(usehttps && usehttpthruhttps==0) {
     if((status=SSL_read(ssl, htmlin,
       sizeof(htmlin)))<1) {
       fprintf(stderr,"\n%s: cannot SSL_read(), status: %d, SSL_error: %d", pr\
ocname, status, SSL_get_error(ssl,status));
       fflush(stderr);
     }
     htmlin[bytes]='\0';
     fprintf(stdout,"\nreceived %d chars, \"%s\"\n", status, htmlin);
   } else {
    if((bytes=read(news, htmlin, sizeof(htmlin)
      ) )==-1) {
      fprintf(stderr,"\n%s: cannot read()\n",procname);
      perror("bind");
      fflush(stderr);
    }
    htmlin[bytes]='\0';
    fprintf(stdout,"\nreceived %d chars,\"%s\"\n", bytes, htmlin);
   }

Eli käytetään SSL_read:id tai read:ia sen mukaan, onko kyseessä https vai http yhteys. htmlin kenttään tulee asiakkaan kenttämuutokset ja nappula ja write:en tai SSL write:en muodostetaan html tai bootstrap tai vastaava lauseita.

Ja vielä lopuksi sivunmuodostus (ja write lauseet):

    html=html1;
    html[0]='\0';

    html_printf("HTTP/1.0 200 OK\r\n");
    html_printf("Location: \r\n");
    html_printf("Server: %s\r\n", programname);
    now = time(NULL);
    strftime(timebuf, sizeof(timebuf), HTMLTIMEFORMAT, gmtime(&now));

    html_printf("Date: %s\r\n", timebuf);

    html=html2;
    html[0]='\0';

    html_printf("\n<!DOCTYPE html>\r\n");
    html_printf("<html lang=\"fi\">");

    html_printf("<head>");
    html_printf("</head>");

    html_printf("<body>");

    html_printf("<h1>Hello, world!</h1>");

    html_printf("</body>");

    html_printf("</html>");

    len=strlen(html2);
    html=html1;
    html_printf("Content-Length: %d",len
    html_printf("\r\n\r\n");

Eli puskuriin 1 laitetaan protokolla, palvelin, kellonaika ja päivämäärä. Kakkospuskuriin laitetaan html lauseita sisältävä sivu (html_printf() on muuten aiemmassa postissa). Ykköspuskuriin laitetaan kakkospuskurin pituuden sisältävä rivi ja seuraavassa write lauseet ykkös ja kakkospuskurille.

   if(usehttps && usehttpthruhttps==0) {
      if((status=SSL_write(ssl, html1, strlen(html1)))<1) {
        fprintf(stderr,"\n%s: cannot SSL_write(), status: %d, SSL error: %d",
                procname, status, SSL_get_error(ssl,status));
        fflush(stderr);
      }
      if((status=SSL_write(ssl, html2, strlen(html2)))<1) {
        fprintf(stderr,"\n%s: cannot SSL_write(), status: %d, SSL error: %d",
                procname, status, SSL_get_error(ssl,status));
        fflush(stderr);
      }
    } else {
      if((bytes=write(news, html1, strlen(html1)))==-1) {
        fprintf(stderr,"\n%s: cannot write()\n", procname);
        perror("write");
        fflush(stderr);
      }
      if((bytes=write(news, html2, strlen(html2)))==-1) {
        fprintf(stderr,"\n%s: cannot write()\n", procname);
        perror("write");
        fflush(stderr);
      }
    }

Kirjoitetaan ykkös ja kakkospuskurit joko SSL_write:llä tai writellä().

Ja lopetetaan tämä input output askel vapauttamalla tuo ssl ctx alue ja suljetaan kierroksen alussa acceptilta saatu news tiedosto.

    if(usehttps && usehttpthruhttps==0) {
      fprintf(stdout,"\nSSL connection using %s", SSL_get_cipher(ssl));
      fflush(stderr);
      SSL_free(ssl);
    }
    if(close(news)==-1) {
      fprintf(stderr,"\n%s: cannot close()\n", procname);
      perror("close");
      fflush(stderr);
    }
    fprintf(stdout,"\ncallid: %d\n", callid++);
    fflush(stdout);
  } /* takaisin for(;;) luupin alkuun */

Jos meillä on tästä for(;;) luupista lopetusmahdollisuus, lopetuksessa vielä vapautetaan SSL_CTX_free komennolla ctx muuttuja.

SSL_CTX_free(ctx);