/* Copyright (c) 2017 - 2021 LiteSpeed Technologies Inc. See LICENSE. */ /* * http_server.c -- A simple HTTP/QUIC server * * It serves up files from the filesystem. */ #include #include #include #include #include #include #include #include #include #ifndef WIN32 #include #include #include #else #include "vc_compat.h" #include "getopt.h" #endif #include #include #include "lsquic.h" #include "../src/liblsquic/lsquic_hash.h" #include "lsxpack_header.h" #include "test_config.h" #include "test_common.h" #include "test_cert.h" #include "prog.h" #if HAVE_REGEX #ifndef WIN32 #include #else #include #endif #endif #include "../src/liblsquic/lsquic_logger.h" #include "../src/liblsquic/lsquic_int_types.h" #include "../src/liblsquic/lsquic_util.h" #if HAVE_REGEX static const char on_being_idle[] = "ON BEING IDLE.\n" "\n" "Now, this is a subject on which I flatter myself I really am _au fait_.\n" "The gentleman who, when I was young, bathed me at wisdom's font for nine\n" "guineas a term--no extras--used to say he never knew a boy who could\n" "do less work in more time; and I remember my poor grandmother once\n" "incidentally observing, in the course of an instruction upon the use\n" "of the Prayer-book, that it was highly improbable that I should ever do\n" "much that I ought not to do, but that she felt convinced beyond a doubt\n" "that I should leave undone pretty well everything that I ought to do.\n" "\n" "I am afraid I have somewhat belied half the dear old lady's prophecy.\n" "Heaven help me! I have done a good many things that I ought not to have\n" "done, in spite of my laziness. But I have fully confirmed the accuracy\n" "of her judgment so far as neglecting much that I ought not to have\n" "neglected is concerned. Idling always has been my strong point. I take\n" "no credit to myself in the matter--it is a gift. Few possess it. There\n" "are plenty of lazy people and plenty of slow-coaches, but a genuine\n" "idler is a rarity. He is not a man who slouches about with his hands in\n" "his pockets. On the contrary, his most startling characteristic is that\n" "he is always intensely busy.\n" "\n" "It is impossible to enjoy idling thoroughly unless one has plenty of\n" "work to do. There is no fun in doing nothing when you have nothing to\n" "do. Wasting time is merely an occupation then, and a most exhausting\n" "one. Idleness, like kisses, to be sweet must be stolen.\n" "\n" "Many years ago, when I was a young man, I was taken very ill--I never\n" "could see myself that much was the matter with me, except that I had\n" "a beastly cold. But I suppose it was something very serious, for the\n" "doctor said that I ought to have come to him a month before, and that\n" "if it (whatever it was) had gone on for another week he would not have\n" "answered for the consequences. It is an extraordinary thing, but I\n" "never knew a doctor called into any case yet but what it transpired\n" "that another day's delay would have rendered cure hopeless. Our medical\n" "guide, philosopher, and friend is like the hero in a melodrama--he\n" "always comes upon the scene just, and only just, in the nick of time. It\n" "is Providence, that is what it is.\n" "\n" "Well, as I was saying, I was very ill and was ordered to Buxton for a\n" "month, with strict injunctions to do nothing whatever all the while\n" "that I was there. \"Rest is what you require,\" said the doctor, \"perfect\n" "rest.\"\n" "\n" "It seemed a delightful prospect. \"This man evidently understands my\n" "complaint,\" said I, and I pictured to myself a glorious time--a four\n" "weeks' _dolce far niente_ with a dash of illness in it. Not too much\n" "illness, but just illness enough--just sufficient to give it the flavor\n" "of suffering and make it poetical. I should get up late, sip chocolate,\n" "and have my breakfast in slippers and a dressing-gown. I should lie out\n" "in the garden in a hammock and read sentimental novels with a melancholy\n" "ending, until the books should fall from my listless hand, and I should\n" "recline there, dreamily gazing into the deep blue of the firmament,\n" "watching the fleecy clouds floating like white-sailed ships across\n" "its depths, and listening to the joyous song of the birds and the low\n" "rustling of the trees. Or, on becoming too weak to go out of doors,\n" "I should sit propped up with pillows at the open window of the\n" "ground-floor front, and look wasted and interesting, so that all the\n" "pretty girls would sigh as they passed by.\n" "\n" "And twice a day I should go down in a Bath chair to the Colonnade to\n" "drink the waters. Oh, those waters! I knew nothing about them then,\n" "and was rather taken with the idea. \"Drinking the waters\" sounded\n" "fashionable and Queen Anne-fied, and I thought I should like them. But,\n" "ugh! after the first three or four mornings! Sam Weller's description of\n" "them as \"having a taste of warm flat-irons\" conveys only a faint idea of\n" "their hideous nauseousness. If anything could make a sick man get well\n" "quickly, it would be the knowledge that he must drink a glassful of them\n" "every day until he was recovered. I drank them neat for six consecutive\n" "days, and they nearly killed me; but after then I adopted the plan of\n" "taking a stiff glass of brandy-and-water immediately on the top of them,\n" "and found much relief thereby. I have been informed since, by various\n" "eminent medical gentlemen, that the alcohol must have entirely\n" "counteracted the effects of the chalybeate properties contained in the\n" "water. I am glad I was lucky enough to hit upon the right thing.\n" "\n" "But \"drinking the waters\" was only a small portion of the torture I\n" "experienced during that memorable month--a month which was, without\n" "exception, the most miserable I have ever spent. During the best part of\n" "it I religiously followed the doctor's mandate and did nothing whatever,\n" "except moon about the house and garden and go out for two hours a day in\n" "a Bath chair. That did break the monotony to a certain extent. There is\n" "more excitement about Bath-chairing--especially if you are not used to\n" "the exhilarating exercise--than might appear to the casual observer. A\n" "sense of danger, such as a mere outsider might not understand, is ever\n" "present to the mind of the occupant. He feels convinced every minute\n" "that the whole concern is going over, a conviction which becomes\n" "especially lively whenever a ditch or a stretch of newly macadamized\n" "road comes in sight. Every vehicle that passes he expects is going to\n" "run into him; and he never finds himself ascending or descending a\n" "hill without immediately beginning to speculate upon his chances,\n" "supposing--as seems extremely probable--that the weak-kneed controller\n" "of his destiny should let go.\n" "\n" "But even this diversion failed to enliven after awhile, and the _ennui_\n" "became perfectly unbearable. I felt my mind giving way under it. It is\n" "not a strong mind, and I thought it would be unwise to tax it too far.\n" "So somewhere about the twentieth morning I got up early, had a good\n" "breakfast, and walked straight off to Hayfield, at the foot of the\n" "Kinder Scout--a pleasant, busy little town, reached through a lovely\n" "valley, and with two sweetly pretty women in it. At least they were\n" "sweetly pretty then; one passed me on the bridge and, I think, smiled;\n" "and the other was standing at an open door, making an unremunerative\n" "investment of kisses upon a red-faced baby. But it is years ago, and I\n" "dare say they have both grown stout and snappish since that time.\n" "Coming back, I saw an old man breaking stones, and it roused such strong\n" "longing in me to use my arms that I offered him a drink to let me take\n" "his place. He was a kindly old man and he humored me. I went for those\n" "stones with the accumulated energy of three weeks, and did more work in\n" "half an hour than he had done all day. But it did not make him jealous.\n" "\n" "Having taken the plunge, I went further and further into dissipation,\n" "going out for a long walk every morning and listening to the band in\n" "the pavilion every evening. But the days still passed slowly\n" "notwithstanding, and I was heartily glad when the last one came and I\n" "was being whirled away from gouty, consumptive Buxton to London with its\n" "stern work and life. I looked out of the carriage as we rushed through\n" "Hendon in the evening. The lurid glare overhanging the mighty city\n" "seemed to warm my heart, and when, later on, my cab rattled out of St.\n" "Pancras' station, the old familiar roar that came swelling up around me\n" "sounded the sweetest music I had heard for many a long day.\n" "\n" "I certainly did not enjoy that month's idling. I like idling when I\n" "ought not to be idling; not when it is the only thing I have to do. That\n" "is my pig-headed nature. The time when I like best to stand with my\n" "back to the fire, calculating how much I owe, is when my desk is heaped\n" "highest with letters that must be answered by the next post. When I like\n" "to dawdle longest over my dinner is when I have a heavy evening's work\n" "before me. And if, for some urgent reason, I ought to be up particularly\n" "early in the morning, it is then, more than at any other time, that I\n" "love to lie an extra half-hour in bed.\n" "\n" "Ah! how delicious it is to turn over and go to sleep again: \"just for\n" "five minutes.\" Is there any human being, I wonder, besides the hero of\n" "a Sunday-school \"tale for boys,\" who ever gets up willingly? There\n" "are some men to whom getting up at the proper time is an utter\n" "impossibility. If eight o'clock happens to be the time that they should\n" "turn out, then they lie till half-past. If circumstances change and\n" "half-past eight becomes early enough for them, then it is nine before\n" "they can rise. They are like the statesman of whom it was said that he\n" "was always punctually half an hour late. They try all manner of schemes.\n" "They buy alarm-clocks (artful contrivances that go off at the wrong time\n" "and alarm the wrong people). They tell Sarah Jane to knock at the door\n" "and call them, and Sarah Jane does knock at the door and does call them,\n" "and they grunt back \"awri\" and then go comfortably to sleep again. I\n" "knew one man who would actually get out and have a cold bath; and even\n" "that was of no use, for afterward he would jump into bed again to warm\n" "himself.\n" "\n" "I think myself that I could keep out of bed all right if I once got\n" "out. It is the wrenching away of the head from the pillow that I find so\n" "hard, and no amount of over-night determination makes it easier. I say\n" "to myself, after having wasted the whole evening, \"Well, I won't do\n" "any more work to-night; I'll get up early to-morrow morning;\" and I am\n" "thoroughly resolved to do so--then. In the morning, however, I feel less\n" "enthusiastic about the idea, and reflect that it would have been much\n" "better if I had stopped up last night. And then there is the trouble of\n" "dressing, and the more one thinks about that the more one wants to put\n" "it off.\n" "\n" "It is a strange thing this bed, this mimic grave, where we stretch our\n" "tired limbs and sink away so quietly into the silence and rest. \"O bed,\n" "O bed, delicious bed, that heaven on earth to the weary head,\" as sang\n" "poor Hood, you are a kind old nurse to us fretful boys and girls. Clever\n" "and foolish, naughty and good, you take us all in your motherly lap and\n" "hush our wayward crying. The strong man full of care--the sick man\n" "full of pain--the little maiden sobbing for her faithless lover--like\n" "children we lay our aching heads on your white bosom, and you gently\n" "soothe us off to by-by.\n" "\n" "Our trouble is sore indeed when you turn away and will not comfort us.\n" "How long the dawn seems coming when we cannot sleep! Oh! those hideous\n" "nights when we toss and turn in fever and pain, when we lie, like living\n" "men among the dead, staring out into the dark hours that drift so slowly\n" "between us and the light. And oh! those still more hideous nights when\n" "we sit by another in pain, when the low fire startles us every now and\n" "then with a falling cinder, and the tick of the clock seems a hammer\n" "beating out the life that we are watching.\n" "\n" "But enough of beds and bedrooms. I have kept to them too long, even for\n" "an idle fellow. Let us come out and have a smoke. That wastes time just\n" "as well and does not look so bad. Tobacco has been a blessing to us\n" "idlers. What the civil-service clerk before Sir Walter's time found\n" "to occupy their minds with it is hard to imagine. I attribute the\n" "quarrelsome nature of the Middle Ages young men entirely to the want of\n" "the soothing weed. They had no work to do and could not smoke, and\n" "the consequence was they were forever fighting and rowing. If, by any\n" "extraordinary chance, there was no war going, then they got up a deadly\n" "family feud with the next-door neighbor, and if, in spite of this, they\n" "still had a few spare moments on their hands, they occupied them with\n" "discussions as to whose sweetheart was the best looking, the arguments\n" "employed on both sides being battle-axes, clubs, etc. Questions of taste\n" "were soon decided in those days. When a twelfth-century youth fell in\n" "love he did not take three paces backward, gaze into her eyes, and tell\n" "her she was too beautiful to live. He said he would step outside and see\n" "about it. And if, when he got out, he met a man and broke his head--the\n" "other man's head, I mean--then that proved that his--the first\n" "fellow's--girl was a pretty girl. But if the other fellow broke _his_\n" "head--not his own, you know, but the other fellow's--the other fellow\n" "to the second fellow, that is, because of course the other fellow would\n" "only be the other fellow to him, not the first fellow who--well, if he\n" "broke his head, then _his_ girl--not the other fellow's, but the fellow\n" "who _was_ the--Look here, if A broke B's head, then A's girl was a\n" "pretty girl; but if B broke A's head, then A's girl wasn't a pretty\n" "girl, but B's girl was. That was their method of conducting art\n" "criticism.\n" "\n" "Nowadays we light a pipe and let the girls fight it out among\n" "themselves.\n" "\n" "They do it very well. They are getting to do all our work. They are\n" "doctors, and barristers, and artists. They manage theaters, and promote\n" "swindles, and edit newspapers. I am looking forward to the time when we\n" "men shall have nothing to do but lie in bed till twelve, read two novels\n" "a day, have nice little five-o'clock teas all to ourselves, and tax\n" "our brains with nothing more trying than discussions upon the latest\n" "patterns in trousers and arguments as to what Mr. Jones' coat was\n" "made of and whether it fitted him. It is a glorious prospect--for idle\n" "fellows.\n" "\n\n\n" ; static const size_t IDLE_SIZE = sizeof(on_being_idle) - 1; #endif /* This is the "LSWS" mode: first write is performed immediately, outside * of the on_write() callback. This makes it possible to play with buffered * packet queues. */ static int s_immediate_write; /* Use preadv(2) in conjuction with lsquic_stream_pwritev() to reduce * number of system calls required to read from disk. The actual value * specifies maximum write size. A negative value indicates always to use * the remaining file size. */ static ssize_t s_pwritev; #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define V(v) (v), strlen(v) struct lsquic_conn_ctx; static void interop_server_hset_destroy (void *); struct server_ctx { struct lsquic_conn_ctx *conn_h; lsquic_engine_t *engine; const char *document_root; const char *push_path; struct sport_head sports; struct prog *prog; unsigned max_conn; unsigned n_conn; unsigned n_current_conns; unsigned delay_resp_sec; }; struct lsquic_conn_ctx { lsquic_conn_t *conn; struct server_ctx *server_ctx; enum { RECEIVED_GOAWAY = 1 << 0, } flags; }; static lsquic_conn_ctx_t * http_server_on_new_conn (void *stream_if_ctx, lsquic_conn_t *conn) { struct server_ctx *server_ctx = stream_if_ctx; lsquic_conn_ctx_t *conn_h = malloc(sizeof(*conn_h)); conn_h->conn = conn; conn_h->server_ctx = server_ctx; server_ctx->conn_h = conn_h; ++server_ctx->n_current_conns; return conn_h; } static void http_server_on_goaway (lsquic_conn_t *conn) { lsquic_conn_ctx_t *conn_h = lsquic_conn_get_ctx(conn); conn_h->flags |= RECEIVED_GOAWAY; LSQ_INFO("received GOAWAY"); } static void http_server_on_conn_closed (lsquic_conn_t *conn) { static int stopped; lsquic_conn_ctx_t *conn_h = lsquic_conn_get_ctx(conn); LSQ_INFO("Connection closed"); --conn_h->server_ctx->n_current_conns; if ((conn_h->server_ctx->prog->prog_flags & PROG_FLAG_COOLDOWN) && 0 == conn_h->server_ctx->n_current_conns) { if (!stopped) { stopped = 1; prog_stop(conn_h->server_ctx->prog); } } if (conn_h->server_ctx->max_conn > 0) { ++conn_h->server_ctx->n_conn; LSQ_NOTICE("Connection closed, remaining: %d", conn_h->server_ctx->max_conn - conn_h->server_ctx->n_conn); if (conn_h->server_ctx->n_conn >= conn_h->server_ctx->max_conn) { if (!stopped) { stopped = 1; prog_stop(conn_h->server_ctx->prog); } } } /* No provision is made to stop HTTP server */ free(conn_h); } struct resp { const char *buf; size_t sz; size_t off; }; struct index_html_ctx { struct resp resp; }; struct ver_head_ctx { struct resp resp; unsigned char *req_body; size_t req_sz; /* Expect it to be the same as qif_sz */ }; struct md5sum_ctx { char resp_buf[0x100]; MD5_CTX md5ctx; struct resp resp; int done; }; struct req { enum method { UNSET, GET, POST, UNSUPPORTED, } method; enum { HAVE_XHDR = 1 << 0, } flags; char *path; char *method_str; char *authority_str; char *qif_str; size_t qif_sz; struct lsxpack_header xhdr; size_t decode_off; char decode_buf[MIN(LSXPACK_MAX_STRLEN + 1, 64 * 1024)]; }; struct interop_push_path { STAILQ_ENTRY(interop_push_path) next; char path[0]; }; struct gen_file_ctx { STAILQ_HEAD(, interop_push_path) push_paths; size_t remain; unsigned idle_off; }; struct lsquic_stream_ctx { lsquic_stream_t *stream; struct server_ctx *server_ctx; FILE *req_fh; char *req_buf; char *req_filename; char *req_path; size_t req_sz; enum { SH_HEADERS_SENT = (1 << 0), SH_DELAYED = (1 << 1), SH_HEADERS_READ = (1 << 2), } flags; struct lsquic_reader reader; int file_fd; /* Used by pwritev */ /* Fields below are used by interop callbacks: */ enum interop_handler { IOH_ERROR, IOH_INDEX_HTML, IOH_MD5SUM, IOH_VER_HEAD, IOH_GEN_FILE, IOH_ECHO, } interop_handler; struct req *req; const char *resp_status; union { struct index_html_ctx ihc; struct ver_head_ctx vhc; struct md5sum_ctx md5c; struct gen_file_ctx gfc; struct { char buf[0x100]; struct resp resp; } err; } interop_u; struct event *resume_resp; size_t written; size_t file_size; /* Used by pwritev */ }; static lsquic_stream_ctx_t * http_server_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) { lsquic_stream_ctx_t *st_h = calloc(1, sizeof(*st_h)); st_h->stream = stream; st_h->server_ctx = stream_if_ctx; lsquic_stream_wantread(stream, 1); return st_h; } static int ends_with (const char *filename, const char *ext) { const char *where; where = strstr(filename, ext); return where && strlen(where) == strlen(ext); } static const char * select_content_type (lsquic_stream_ctx_t *st_h) { if ( ends_with(st_h->req_filename, ".html")) return "text/html"; else if (ends_with(st_h->req_filename, ".png")) return "image/png"; else if (ends_with(st_h->req_filename, ".css")) return "text/css"; else if (ends_with(st_h->req_filename, ".gif")) return "image/gif"; else if (ends_with(st_h->req_filename, ".txt")) return "text/plain"; else return "application/octet-stream"; } static int send_headers (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { const char *content_type; struct header_buf hbuf; content_type = select_content_type(st_h); struct lsxpack_header headers_arr[2]; hbuf.off = 0; header_set_ptr(&headers_arr[0], &hbuf, ":status", 7, "200", 3); header_set_ptr(&headers_arr[1], &hbuf, "content-type", 12, content_type, strlen(content_type)); lsquic_http_headers_t headers = { .count = sizeof(headers_arr) / sizeof(headers_arr[0]), .headers = headers_arr, }; if (0 != lsquic_stream_send_headers(stream, &headers, 0)) { LSQ_ERROR("cannot send headers: %s", strerror(errno)); return -1; } st_h->flags |= SH_HEADERS_SENT; return 0; } static void resume_response (evutil_socket_t fd, short what, void *arg) { struct lsquic_stream_ctx *const st_h = arg; lsquic_stream_wantwrite(st_h->stream, 1); event_del(st_h->resume_resp); event_free(st_h->resume_resp); st_h->resume_resp = NULL; LSQ_NOTICE("resume response to stream %"PRIu64, lsquic_stream_id(st_h->stream)); prog_process_conns(st_h->server_ctx->prog); } static size_t bytes_left (lsquic_stream_ctx_t *st_h) { if (s_pwritev) return st_h->file_size - st_h->written; else return test_reader_size(st_h->reader.lsqr_ctx); } static ssize_t my_preadv (void *user_data, const struct iovec *iov, int iovcnt) { #if HAVE_PREADV lsquic_stream_ctx_t *const st_h = user_data; ssize_t nread = preadv(st_h->file_fd, iov, iovcnt, st_h->written); LSQ_DEBUG("%s: wrote %zd bytes", __func__, (size_t) nread); return nread; #else return -1; #endif } static size_t pwritev_fallback_read (void *lsqr_ctx, void *buf, size_t count) { lsquic_stream_ctx_t *const st_h = lsqr_ctx; struct iovec iov; size_t ntoread; ntoread = st_h->file_size - st_h->written; if (ntoread > count) count = ntoread; iov.iov_base = buf; iov.iov_len = count; return my_preadv(lsqr_ctx, &iov, 1); } static size_t pwritev_fallback_size (void *lsqr_ctx) { lsquic_stream_ctx_t *const st_h = lsqr_ctx; return st_h->file_size - st_h->written; } static void http_server_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { if (st_h->flags & SH_HEADERS_SENT) { ssize_t nw; if (bytes_left(st_h) > 0) { if (st_h->server_ctx->delay_resp_sec && !(st_h->flags & SH_DELAYED) && st_h->written > 10000000) { struct timeval delay = { .tv_sec = st_h->server_ctx->delay_resp_sec, }; st_h->resume_resp = event_new(st_h->server_ctx->prog->prog_eb, -1, EV_TIMEOUT, resume_response, st_h); if (st_h->resume_resp) { event_add(st_h->resume_resp, &delay); lsquic_stream_wantwrite(stream, 0); st_h->flags |= SH_DELAYED; LSQ_NOTICE("delay response of stream %"PRIu64" for %u seconds", lsquic_stream_id(stream), st_h->server_ctx->delay_resp_sec); return; } else LSQ_ERROR("cannot allocate event"); } if (s_pwritev) { size_t to_write = bytes_left(st_h); if (s_pwritev > 0 && (size_t) s_pwritev < to_write) to_write = s_pwritev; nw = lsquic_stream_pwritev(stream, my_preadv, st_h, to_write); if (nw == 0) { struct lsquic_reader reader = { .lsqr_read = pwritev_fallback_read, .lsqr_size = pwritev_fallback_size, .lsqr_ctx = st_h, }; nw = lsquic_stream_writef(stream, &reader); } } else { nw = lsquic_stream_writef(stream, &st_h->reader); } if (nw < 0) { struct lsquic_conn *conn = lsquic_stream_conn(stream); lsquic_conn_ctx_t *conn_h = lsquic_conn_get_ctx(conn); if (conn_h->flags & RECEIVED_GOAWAY) { LSQ_NOTICE("cannot write: goaway received"); lsquic_stream_close(stream); } else { LSQ_ERROR("write error: %s", strerror(errno)); exit(1); } } if (bytes_left(st_h) > 0) { st_h->written += (size_t) nw; lsquic_stream_wantwrite(stream, 1); } else { lsquic_stream_shutdown(stream, 1); lsquic_stream_wantread(stream, 1); } } else { lsquic_stream_shutdown(stream, 1); lsquic_stream_wantread(stream, 1); } } else { if (0 != send_headers(stream, st_h)) exit(1); } } struct capped_reader_ctx { struct lsquic_reader *inner_reader; size_t nread; }; static size_t capped_reader_size (void *void_ctx) { struct capped_reader_ctx *const capped_reader_ctx = void_ctx; struct lsquic_reader *const inner_reader = capped_reader_ctx->inner_reader; size_t size; size = inner_reader->lsqr_size(inner_reader->lsqr_ctx); return MIN((size_t) (s_immediate_write - capped_reader_ctx->nread), size); } static size_t capped_reader_read (void *void_ctx, void *buf, size_t count) { struct capped_reader_ctx *const capped_reader_ctx = void_ctx; struct lsquic_reader *const inner_reader = capped_reader_ctx->inner_reader; size_t size; count = MIN(count, (size_t) (s_immediate_write - capped_reader_ctx->nread)); size = inner_reader->lsqr_read(inner_reader->lsqr_ctx, buf, count); capped_reader_ctx->nread += size; return size; } #if HAVE_OPEN_MEMSTREAM static void parse_request (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { char *filename; int s; regex_t re; regmatch_t matches[2]; s = regcomp(&re, "GET (.*) HTTP/1.[01]\r\n", REG_EXTENDED); if (0 != s) { perror("regcomp"); exit(3); } s = regexec(&re, st_h->req_buf, 2, matches, 0); if (0 != s) { LSQ_WARN("GET request could not be parsed: `%s'", st_h->req_buf); regfree(&re); return; } regfree(&re); filename = malloc(strlen(st_h->server_ctx->document_root) + 1 + matches[1].rm_eo - matches[1].rm_so + 1); strcpy(filename, st_h->server_ctx->document_root); strcat(filename, "/"); strncat(filename, st_h->req_buf + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so); LSQ_INFO("filename to fetch: %s", filename); st_h->req_filename = filename; st_h->req_path = strdup(filename); } static void process_request (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { struct stat st; if (s_pwritev) { st_h->file_fd = open(st_h->req_path, O_RDONLY); if (st_h->file_fd < 0) { LSQ_ERROR("cannot open %s for reading: %s", st_h->req_path, strerror(errno)); exit(1); } if (fstat(st_h->file_fd, &st) < 0) { LSQ_ERROR("fstat: %s", strerror(errno)); exit(1); } st_h->file_size = st.st_size; } else { st_h->reader.lsqr_read = test_reader_read; st_h->reader.lsqr_size = test_reader_size; st_h->reader.lsqr_ctx = create_lsquic_reader_ctx(st_h->req_path); if (!st_h->reader.lsqr_ctx) exit(1); } if (s_immediate_write) { if (0 != send_headers(stream, st_h)) exit(1); if (test_reader_size(st_h->reader.lsqr_ctx) > 0) { struct capped_reader_ctx capped_reader_ctx = { .inner_reader = &st_h->reader, }; struct lsquic_reader capped_reader = { .lsqr_read = capped_reader_read, .lsqr_size = capped_reader_size, .lsqr_ctx = &capped_reader_ctx, }; ssize_t nw; nw = lsquic_stream_writef(stream, &capped_reader); if (nw < 0) { LSQ_ERROR("write error: %s", strerror(errno)); exit(1); } } if (test_reader_size(st_h->reader.lsqr_ctx) > 0) { lsquic_stream_flush(stream); lsquic_stream_wantwrite(stream, 1); } else { lsquic_stream_shutdown(stream, 1); lsquic_stream_wantread(stream, 1); } } else lsquic_stream_wantwrite(st_h->stream, 1); } static struct hset_fm /* FM stands for Filesystem Mode */ { unsigned id; char *path; } * new_hset_fm (const char *path) { static unsigned hfm_id; struct hset_fm *const hfm = malloc(sizeof(*hfm)); char *const str = strdup(path); if (hfm && path) { hfm->id = hfm_id++; hfm->path = str; return hfm; } else { free(str); free(hfm); return NULL; } } static void destroy_hset_fm (struct hset_fm *hfm) { free(hfm->path); free(hfm); } static int push_promise (lsquic_stream_ctx_t *st_h, lsquic_stream_t *stream) { lsquic_conn_t *conn; int s; regex_t re; regmatch_t matches[2]; struct hset_fm *hfm; struct header_buf hbuf; s = regcomp(&re, "\r\nHost: *([[:alnum:].][[:alnum:].]*)\r\n", REG_EXTENDED|REG_ICASE); if (0 != s) { perror("regcomp"); exit(3); } s = regexec(&re, st_h->req_buf, 2, matches, 0); if (0 != s) { LSQ_WARN("Could not find host header in request `%s'", st_h->req_buf); regfree(&re); return -1; } regfree(&re); hfm = new_hset_fm(st_h->server_ctx->push_path); if (!hfm) { LSQ_WARN("Could not allocate hfm"); return -1; } #define V(v) (v), strlen(v) hbuf.off = 0; struct lsxpack_header headers_arr[6]; header_set_ptr(&headers_arr[0], &hbuf, V(":method"), V("GET")); header_set_ptr(&headers_arr[1], &hbuf, V(":path"), V(st_h->server_ctx->push_path)); header_set_ptr(&headers_arr[2], &hbuf, V(":authority"), st_h->req_buf + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so); header_set_ptr(&headers_arr[3], &hbuf, V(":scheme"), V("https")); header_set_ptr(&headers_arr[4], &hbuf, V("x-some-header"), V("x-some-value")); header_set_ptr(&headers_arr[5], &hbuf, V("x-kenny-status"), V("Oh my God! They killed Kenny!!! You bastards!")); lsquic_http_headers_t headers = { .count = sizeof(headers_arr) / sizeof(headers_arr[0]), .headers = headers_arr, }; conn = lsquic_stream_conn(stream); s = lsquic_conn_push_stream(conn, hfm, stream, &headers); if (0 == s) LSQ_NOTICE("pushed stream successfully"); else { destroy_hset_fm(hfm); LSQ_ERROR("could not push stream: %s", strerror(errno)); } return 0; } static void http_server_on_read_pushed (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { struct hset_fm *hfm; hfm = lsquic_stream_get_hset(stream); if (!hfm) { LSQ_ERROR("%s: error fetching hset: %s", __func__, strerror(errno)); lsquic_stream_close(stream); return; } LSQ_INFO("got push request #%u for %s", hfm->id, hfm->path); st_h->req_path = malloc(strlen(st_h->server_ctx->document_root) + 1 + strlen(hfm->path) + 1); strcpy(st_h->req_path, st_h->server_ctx->document_root); strcat(st_h->req_path, "/"); strcat(st_h->req_path, hfm->path); st_h->req_filename = strdup(st_h->req_path); /* XXX Only used for ends_with: drop it? */ process_request(stream, st_h); free(st_h->req_buf); lsquic_stream_shutdown(stream, 0); destroy_hset_fm(hfm); } static void http_server_on_read_regular (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { unsigned char buf[0x400]; ssize_t nread; int s; if (!st_h->req_fh) st_h->req_fh = open_memstream(&st_h->req_buf, &st_h->req_sz); nread = lsquic_stream_read(stream, buf, sizeof(buf)); if (nread > 0) fwrite(buf, 1, nread, st_h->req_fh); else if (0 == nread) { fwrite("", 1, 1, st_h->req_fh); /* NUL-terminate so that we can regex the string */ fclose(st_h->req_fh); LSQ_INFO("got request: `%.*s'", (int) st_h->req_sz, st_h->req_buf); parse_request(stream, st_h); if (st_h->server_ctx->push_path && 0 != strcmp(st_h->req_path, st_h->server_ctx->push_path)) { s = push_promise(st_h, stream); if (s != 0) exit(1); } process_request(stream, st_h); free(st_h->req_buf); lsquic_stream_shutdown(stream, 0); } else { LSQ_ERROR("error reading: %s", strerror(errno)); lsquic_stream_close(stream); } } #endif static void http_server_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *st_h) { #if HAVE_OPEN_MEMSTREAM if (lsquic_stream_is_pushed(stream)) http_server_on_read_pushed(stream, st_h); else http_server_on_read_regular(stream, st_h); #else LSQ_ERROR("%s: open_memstream not supported\n", __func__); exit(1); #endif } static void http_server_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { free(st_h->req_filename); free(st_h->req_path); if (st_h->reader.lsqr_ctx) destroy_lsquic_reader_ctx(st_h->reader.lsqr_ctx); #if HAVE_PREADV if (s_pwritev) close(st_h->file_fd); #endif if (st_h->req) interop_server_hset_destroy(st_h->req); free(st_h); LSQ_INFO("%s called, has unacked data: %d", __func__, lsquic_stream_has_unacked_data(stream)); } const struct lsquic_stream_if http_server_if = { .on_new_conn = http_server_on_new_conn, .on_conn_closed = http_server_on_conn_closed, .on_new_stream = http_server_on_new_stream, .on_read = http_server_on_read, .on_write = http_server_on_write, .on_close = http_server_on_close, .on_goaway_received = http_server_on_goaway, }; #if HAVE_REGEX struct req_map { enum method method; const char *path; enum interop_handler handler; const char *status; enum { RM_WANTBODY = 1 << 0, RM_REGEX = 1 << 1, RM_COMPILED = 1 << 2, } flags; regex_t re; }; static struct req_map req_maps[] = { { .method = GET, .path = "/", .handler = IOH_INDEX_HTML, .status = "200", .flags = 0, }, { .method = GET, .path = "/index.html", .handler = IOH_INDEX_HTML, .status = "200", .flags = 0, }, { .method = POST, .path = "/cgi-bin/md5sum.cgi", .handler = IOH_MD5SUM, .status = "200", .flags = RM_WANTBODY, }, { .method = POST, .path = "/cgi-bin/verify-headers.cgi", .handler = IOH_VER_HEAD, .status = "200", .flags = RM_WANTBODY, }, { .method = GET, .path = "^/([0-9][0-9]*)([KMG]?)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/([0-9][0-9]*)([KMG]?)\\?push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/([0-9][0-9]*)([KMG]?)\\?push=([^&]*)&push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/([0-9][0-9]*)([KMG]?)\\?push=([^&]*)&push=([^&]*)&push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/file-([0-9][0-9]*)([KMG]?)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/file-([0-9][0-9]*)([KMG]?)\\?push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/file-([0-9][0-9]*)([KMG]?)\\?push=([^&]*)&push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, { .method = GET, .path = "^/file-([0-9][0-9]*)([KMG]?)\\?push=([^&]*)&push=([^&]*)&push=([^&]*)$", .handler = IOH_GEN_FILE, .status = "200", .flags = RM_REGEX, }, }; #define MAX_MATCHES 5 static void init_map_regexes (void) { struct req_map *map; for (map = req_maps; map < req_maps + sizeof(req_maps) / sizeof(req_maps[0]); ++map) if (map->flags & RM_REGEX) { #ifndef NDEBUG int s; s = #endif regcomp(&map->re, map->path, REG_EXTENDED|REG_ICASE); assert(0 == s); map->flags |= RM_COMPILED; } } static void free_map_regexes (void) { struct req_map *map; for (map = req_maps; map < req_maps + sizeof(req_maps) / sizeof(req_maps[0]); ++map) if (map->flags & RM_COMPILED) { regfree(&map->re); map->flags &= ~RM_COMPILED; } } static const struct req_map * find_handler (enum method method, const char *path, regmatch_t *matches) { const struct req_map *map; for (map = req_maps; map < req_maps + sizeof(req_maps) / sizeof(req_maps[0]); ++map) if (map->flags & RM_COMPILED) { if (0 == regexec(&map->re, path, MAX_MATCHES + 1, matches, 0)) return map; } else if (0 == strcasecmp(path, map->path)) return map; return NULL; } static const char INDEX_HTML[] = "\n" " \n" " LiteSpeed IETF QUIC Server Index Page\n" " \n" " \n" "

LiteSpeed IETF QUIC Server Index Page

\n" "

Hello! Welcome to the interop. Available services:\n" "

    \n" "
  • POST to /cgi-bin/md5sum.cgi. This will return\n" " MD5 checksum of the request body.\n" "
  • GET /123K or GET /file-123K. This will return\n" " requested number of payload in the form of repeating text\n" " by Jerome K. Jerome. The size specification must match\n" " (\\d+)[KMG]? and the total size request must not exceed\n" " 2 gigabytes. Then, you will get back that many bytes\n" " of the beloved classic.\n" "
\n" " \n" "\n" ; static size_t read_md5 (void *ctx, const unsigned char *buf, size_t sz, int fin) { struct lsquic_stream_ctx *st_h = ctx; if (sz) MD5_Update(&st_h->interop_u.md5c.md5ctx, buf, sz); if (fin) st_h->interop_u.md5c.done = 1; return sz; } static void http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { #define ERROR_RESP(code, ...) do { \ LSQ_WARN(__VA_ARGS__); \ st_h->interop_handler = IOH_ERROR; \ st_h->resp_status = #code; \ st_h->interop_u.err.resp.sz = snprintf(st_h->interop_u.err.buf, \ sizeof(st_h->interop_u.err.buf), __VA_ARGS__); \ if (st_h->interop_u.err.resp.sz >= sizeof(st_h->interop_u.err.buf)) \ st_h->interop_u.err.resp.sz = sizeof(st_h->interop_u.err.buf) - 1; \ st_h->interop_u.err.resp.buf = st_h->interop_u.err.buf; \ st_h->interop_u.err.resp.off = 0; \ goto err; \ } while (0) const struct req_map *map; ssize_t nw; size_t need; unsigned len, i; struct interop_push_path *push_path; regmatch_t matches[MAX_MATCHES + 1]; unsigned char md5sum[MD5_DIGEST_LENGTH]; char md5str[ sizeof(md5sum) * 2 + 1 ]; char byte[1]; if (!(st_h->flags & SH_HEADERS_READ)) { st_h->flags |= SH_HEADERS_READ; st_h->req = lsquic_stream_get_hset(stream); if (!st_h->req) ERROR_RESP(500, "Internal error: cannot fetch header set from stream"); else if (st_h->req->method == UNSET) ERROR_RESP(400, "Method is not specified"); else if (!st_h->req->path) ERROR_RESP(400, "Path is not specified"); else if (st_h->req->method == UNSUPPORTED) ERROR_RESP(501, "Method %s is not supported", st_h->req->method_str); else if (!(map = find_handler(st_h->req->method, st_h->req->path, matches))) ERROR_RESP(404, "No handler found for method: %s; path: %s", st_h->req->method_str, st_h->req->path); else { LSQ_INFO("found handler for %s %s", st_h->req->method_str, st_h->req->path); st_h->resp_status = map->status; st_h->interop_handler = map->handler; switch (map->handler) { case IOH_INDEX_HTML: st_h->interop_u.ihc.resp = (struct resp) { INDEX_HTML, sizeof(INDEX_HTML) - 1, 0, }; break; case IOH_VER_HEAD: st_h->interop_u.vhc.resp = (struct resp) { st_h->req->qif_str, st_h->req->qif_sz, 0, }; break; case IOH_MD5SUM: MD5_Init(&st_h->interop_u.md5c.md5ctx); st_h->interop_u.md5c.done = 0; break; case IOH_GEN_FILE: STAILQ_INIT(&st_h->interop_u.gfc.push_paths); st_h->interop_u.gfc.remain = strtol(st_h->req->path + matches[1].rm_so, NULL, 10); if (matches[2].rm_so >= 0 && matches[2].rm_so < matches[2].rm_eo) { switch (st_h->req->path[ matches[2].rm_so ]) { case 'G': case 'g': st_h->interop_u.gfc.remain <<= 30; break; case 'M': case 'm': st_h->interop_u.gfc.remain <<= 20; break; case 'K': case 'k': st_h->interop_u.gfc.remain <<= 10; break; } } if (st_h->interop_u.gfc.remain > 2 * (1u << 30)) ERROR_RESP(406, "Response of %zd bytes is too long to generate", st_h->interop_u.gfc.remain); st_h->interop_u.gfc.idle_off = 0; for (i = 3; i <= MAX_MATCHES; ++i) if (matches[i].rm_so >= 0) { len = matches[i].rm_eo - matches[i].rm_so; push_path = malloc(sizeof(*push_path) + len + 1); memcpy(push_path->path, st_h->req->path + matches[i].rm_so, len); push_path->path[len] ='\0'; STAILQ_INSERT_TAIL(&st_h->interop_u.gfc.push_paths, push_path, next); } else break; break; default: /* TODO: implement this */ assert(0); break; } } if (!(map->flags & RM_WANTBODY)) { err: lsquic_stream_shutdown(stream, 0); lsquic_stream_wantwrite(stream, 1); } } else { switch (st_h->interop_handler) { case IOH_MD5SUM: assert(!st_h->interop_u.md5c.done); nw = lsquic_stream_readf(stream, read_md5, st_h); if (nw < 0) { LSQ_ERROR("could not read from stream for MD5: %s", strerror(errno)); exit(1); } if (nw == 0) st_h->interop_u.md5c.done = 1; if (st_h->interop_u.md5c.done) { MD5_Final(md5sum, &st_h->interop_u.md5c.md5ctx); lsquic_hexstr(md5sum, sizeof(md5sum), md5str, sizeof(md5str)); snprintf(st_h->interop_u.md5c.resp_buf, sizeof(st_h->interop_u.md5c.resp_buf), "MD5 Checksum Result\n" "

MD5 Checksum Result

\n

" "MD5 Checksum: %s\n\n", md5str); st_h->interop_u.md5c.resp.buf = st_h->interop_u.md5c.resp_buf; st_h->interop_u.md5c.resp.sz = strlen(st_h->interop_u.md5c.resp_buf); st_h->interop_u.md5c.resp.off = 0; lsquic_stream_shutdown(stream, 0); lsquic_stream_wantwrite(stream, 1); } break; case IOH_VER_HEAD: if (!st_h->interop_u.vhc.req_body) { st_h->interop_u.vhc.req_body = malloc(st_h->req->qif_sz); if (!st_h->interop_u.vhc.req_body) { perror("malloc"); exit(1); } } need = st_h->req->qif_sz - st_h->interop_u.vhc.req_sz; if (need > 0) { nw = lsquic_stream_read(stream, st_h->interop_u.vhc.req_body + st_h->interop_u.vhc.req_sz, need); if (nw > 0) st_h->interop_u.vhc.req_sz += need; else if (nw == 0) { LSQ_WARN("request body too short (does not match headers)"); lsquic_stream_shutdown(stream, 0); lsquic_stream_wantwrite(stream, 1); } else { LSQ_ERROR("error reading from stream"); exit(1); } } else { nw = lsquic_stream_read(stream, byte, sizeof(byte)); if (nw == 0) { if (0 == memcmp(st_h->req->qif_str, st_h->interop_u.vhc.req_body, st_h->req->qif_sz)) LSQ_INFO("request headers and payload check out"); else LSQ_WARN("request headers and payload are different"); } else LSQ_WARN("request body too long (does not match headers)"); lsquic_stream_shutdown(stream, 0); lsquic_stream_wantwrite(stream, 1); } break; default: assert(0); } } } static int send_headers2 (struct lsquic_stream *stream, struct lsquic_stream_ctx *st_h, size_t content_len) { char clbuf[0x20]; struct header_buf hbuf; snprintf(clbuf, sizeof(clbuf), "%zd", content_len); hbuf.off = 0; struct lsxpack_header headers_arr[4]; header_set_ptr(&headers_arr[0], &hbuf, V(":status"), V(st_h->resp_status)); header_set_ptr(&headers_arr[1], &hbuf, V("server"), V(LITESPEED_ID)); header_set_ptr(&headers_arr[2], &hbuf, V("content-type"), V("text/html")); header_set_ptr(&headers_arr[3], &hbuf, V("content-length"), V(clbuf)); lsquic_http_headers_t headers = { .count = sizeof(headers_arr) / sizeof(headers_arr[0]), .headers = headers_arr, }; return lsquic_stream_send_headers(st_h->stream, &headers, 0); } #define MIN(a, b) ((a) < (b) ? (a) : (b)) static size_t idle_read (void *lsqr_ctx, void *buf, size_t count) { struct gen_file_ctx *const gfc = lsqr_ctx; unsigned char *p = buf; unsigned char *const end = p + count; size_t towrite; while (p < end && gfc->remain > 0) { towrite = MIN((unsigned) (end - p), IDLE_SIZE - gfc->idle_off); if (towrite > gfc->remain) towrite = gfc->remain; memcpy(p, on_being_idle + gfc->idle_off, towrite); gfc->idle_off += towrite; if (gfc->idle_off == IDLE_SIZE) gfc->idle_off = 0; p += towrite; gfc->remain -= towrite; } return p - (unsigned char *) buf; } static size_t idle_size (void *lsqr_ctx) { struct gen_file_ctx *const gfc = lsqr_ctx; return gfc->remain; } static struct req * new_req (enum method method, const char *path, const char *authority) { struct req *req; req = malloc(sizeof(*req)); if (!req) return NULL; memset(req, 0, offsetof(struct req, decode_buf)); req->method = method; req->path = strdup(path); req->authority_str = strdup(authority); if (!(req->path && req->authority_str)) { free(req->path); free(req->authority_str); free(req); return NULL; } return req; } static ssize_t my_interop_preadv (void *user_data, const struct iovec *iov, int iovcnt) { struct gen_file_ctx *const gfc = user_data; size_t nread, nr; int i; nread = 0; for (i = 0; i < iovcnt; ++i) { nr = idle_read(gfc, iov[i].iov_base, iov[i].iov_len); nread += nr; } return (ssize_t) nread; } static void idle_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { struct gen_file_ctx *const gfc = &st_h->interop_u.gfc; struct interop_push_path *push_path; struct lsxpack_header header_arr[4]; struct lsquic_http_headers headers; struct req *req; ssize_t nw; struct header_buf hbuf; struct lsquic_reader reader; if (st_h->flags & SH_HEADERS_SENT) { if (s_pwritev) { nw = lsquic_stream_pwritev(stream, my_interop_preadv, gfc, gfc->remain); if (nw == 0) goto with_reader; } else { with_reader: reader.lsqr_read = idle_read, reader.lsqr_size = idle_size, reader.lsqr_ctx = gfc, nw = lsquic_stream_writef(stream, &reader); } if (nw < 0) { LSQ_ERROR("error writing idle thoughts: %s", strerror(errno)); exit(1); } if (gfc->remain == 0) lsquic_stream_shutdown(stream, 1); } else { if (st_h->req->authority_str) while ((push_path = STAILQ_FIRST(&gfc->push_paths))) { STAILQ_REMOVE_HEAD(&gfc->push_paths, next); LSQ_DEBUG("pushing promise for %s", push_path->path); hbuf.off = 0; header_set_ptr(&header_arr[0], &hbuf, V(":method"), V("GET")); header_set_ptr(&header_arr[1], &hbuf, V(":path"), V(push_path->path)); header_set_ptr(&header_arr[2], &hbuf, V(":authority"), V(st_h->req->authority_str)); header_set_ptr(&header_arr[3], &hbuf, V(":scheme"), V("https")); headers.headers = header_arr; headers.count = sizeof(header_arr) / sizeof(header_arr[0]); req = new_req(GET, push_path->path, st_h->req->authority_str); if (req) { if (0 != lsquic_conn_push_stream(lsquic_stream_conn(stream), req, stream, &headers)) { LSQ_WARN("stream push failed"); interop_server_hset_destroy(req); } } else LSQ_WARN("cannot allocate req for push"); free(push_path); } if (0 == send_headers2(stream, st_h, gfc->remain)) st_h->flags |= SH_HEADERS_SENT; else { LSQ_ERROR("cannot send headers: %s", strerror(errno)); lsquic_stream_close(stream); } } } static void http_server_interop_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { struct resp *resp; ssize_t nw; switch (st_h->interop_handler) { case IOH_ERROR: resp = &st_h->interop_u.err.resp; goto reply; case IOH_INDEX_HTML: resp = &st_h->interop_u.ihc.resp; goto reply; case IOH_VER_HEAD: resp = &st_h->interop_u.vhc.resp; goto reply; case IOH_MD5SUM: resp = &st_h->interop_u.md5c.resp; goto reply; case IOH_GEN_FILE: idle_on_write(stream, st_h); return; default: assert(0); return; } reply: assert(resp->sz); /* We always need to send body */ if (!(st_h->flags & SH_HEADERS_SENT)) { send_headers2(stream, st_h, resp->sz); st_h->flags |= SH_HEADERS_SENT; return; } nw = lsquic_stream_write(stream, resp->buf + resp->off, resp->sz - resp->off); if (nw < 0) { LSQ_ERROR("error writing to stream: %s", strerror(errno)); lsquic_conn_abort(lsquic_stream_conn(stream)); return; } resp->off += nw; lsquic_stream_flush(stream); if (resp->off == resp->sz) lsquic_stream_shutdown(stream, 1); } const struct lsquic_stream_if interop_http_server_if = { .on_new_conn = http_server_on_new_conn, .on_conn_closed = http_server_on_conn_closed, .on_new_stream = http_server_on_new_stream, .on_read = http_server_interop_on_read, .on_write = http_server_interop_on_write, .on_close = http_server_on_close, }; #endif /* HAVE_REGEX */ static void usage (const char *prog) { const char *const slash = strrchr(prog, '/'); if (slash) prog = slash + 1; printf( "Usage: %s [opts]\n" "\n" "Options:\n" " -r ROOT Document root\n" " -p FILE Push request with this path\n" " -w SIZE Write immediately (LSWS mode). Argument specifies maximum\n" " size of the immediate write.\n" #if HAVE_PREADV " -P SIZE Use preadv(2) to read from disk and lsquic_stream_pwritev() to\n" " write to stream. Positive SIZE indicate maximum value per\n" " write; negative means always use remaining file size.\n" " Incompatible with -w.\n" #endif " -y DELAY Delay response for this many seconds -- use for debugging\n" , prog); } static void * interop_server_hset_create (void *hsi_ctx, lsquic_stream_t *stream, int is_push_promise) { struct req *req; req = malloc(sizeof(struct req)); memset(req, 0, offsetof(struct req, decode_buf)); return req; } static struct lsxpack_header * interop_server_hset_prepare_decode (void *hset_p, struct lsxpack_header *xhdr, size_t req_space) { struct req *req = hset_p; if (xhdr) { LSQ_WARN("we don't reallocate headers: can't give more"); return NULL; } if (req->flags & HAVE_XHDR) { if (req->decode_off + lsxpack_header_get_dec_size(&req->xhdr) >= sizeof(req->decode_buf)) { LSQ_WARN("Not enough room in header"); return NULL; } req->decode_off += lsxpack_header_get_dec_size(&req->xhdr); } else req->flags |= HAVE_XHDR; lsxpack_header_prepare_decode(&req->xhdr, req->decode_buf, req->decode_off, sizeof(req->decode_buf) - req->decode_off); return &req->xhdr; } static int interop_server_hset_add_header (void *hset_p, struct lsxpack_header *xhdr) { struct req *req = hset_p; const char *name, *value; unsigned name_len, value_len; if (!xhdr) return 0; name = lsxpack_header_get_name(xhdr); value = lsxpack_header_get_value(xhdr); name_len = xhdr->name_len; value_len = xhdr->val_len; req->qif_str = realloc(req->qif_str, req->qif_sz + name_len + value_len + 2); if (!req->qif_str) { LSQ_ERROR("malloc failed"); return -1; } memcpy(req->qif_str + req->qif_sz, name, name_len); req->qif_str[req->qif_sz + name_len] = '\t'; memcpy(req->qif_str + req->qif_sz + name_len + 1, value, value_len); req->qif_str[req->qif_sz + name_len + 1 + value_len] = '\n'; req->qif_sz += name_len + value_len + 2; if (5 == name_len && 0 == strncmp(name, ":path", 5)) { if (req->path) return 1; req->path = strndup(value, value_len); if (!req->path) return -1; return 0; } if (7 == name_len && 0 == strncmp(name, ":method", 7)) { if (req->method != UNSET) return 1; req->method_str = strndup(value, value_len); if (!req->method_str) return -1; if (0 == strcmp(req->method_str, "GET")) req->method = GET; else if (0 == strcmp(req->method_str, "POST")) req->method = POST; else req->method = UNSUPPORTED; return 0; } if (10 == name_len && 0 == strncmp(name, ":authority", 10)) { req->authority_str = strndup(value, value_len); if (!req->authority_str) return -1; return 0; } return 0; } static void interop_server_hset_destroy (void *hset_p) { struct req *req = hset_p; free(req->qif_str); free(req->path); free(req->method_str); free(req->authority_str); free(req); } static const struct lsquic_hset_if header_bypass_api = { .hsi_create_header_set = interop_server_hset_create, .hsi_prepare_decode = interop_server_hset_prepare_decode, .hsi_process_header = interop_server_hset_add_header, .hsi_discard_header_set = interop_server_hset_destroy, }; int main (int argc, char **argv) { int opt, s; struct stat st; struct server_ctx server_ctx; struct prog prog; const char *const *alpn; #if !(HAVE_OPEN_MEMSTREAM || HAVE_REGEX) fprintf(stderr, "cannot run server without regex or open_memstream\n"); return 1; #endif memset(&server_ctx, 0, sizeof(server_ctx)); TAILQ_INIT(&server_ctx.sports); server_ctx.prog = &prog; prog_init(&prog, LSENG_SERVER|LSENG_HTTP, &server_ctx.sports, &http_server_if, &server_ctx); while (-1 != (opt = getopt(argc, argv, PROG_OPTS "y:Y:n:p:r:w:P:h"))) { switch (opt) { case 'n': server_ctx.max_conn = atoi(optarg); break; case 'p': server_ctx.push_path = optarg; break; case 'r': if (-1 == stat(optarg, &st)) { perror("stat"); exit(2); } #ifndef WIN32 if (!S_ISDIR(st.st_mode)) { fprintf(stderr, "`%s' is not a directory\n", optarg); exit(2); } #endif server_ctx.document_root = optarg; break; case 'w': s_immediate_write = atoi(optarg); break; case 'P': #if HAVE_PREADV s_pwritev = strtoull(optarg, NULL, 10); break; #else fprintf(stderr, "preadv is not supported on this platform, " "cannot use -P\n"); exit(EXIT_FAILURE); #endif case 'y': server_ctx.delay_resp_sec = atoi(optarg); break; case 'h': usage(argv[0]); prog_print_common_options(&prog, stdout); exit(0); default: if (0 != prog_set_opt(&prog, opt, optarg)) exit(1); } } if (!server_ctx.document_root) { #if HAVE_REGEX LSQ_NOTICE("Document root is not set: start in Interop Mode"); init_map_regexes(); prog.prog_api.ea_stream_if = &interop_http_server_if; prog.prog_api.ea_hsi_if = &header_bypass_api; prog.prog_api.ea_hsi_ctx = NULL; #else LSQ_ERROR("Document root is not set: use -r option"); exit(EXIT_FAILURE); #endif } if (s_immediate_write && s_pwritev) { LSQ_ERROR("-w and -P are incompatible options"); exit(EXIT_FAILURE); } alpn = lsquic_get_h3_alpns(prog.prog_settings.es_versions); while (*alpn) { if (0 == add_alpn(*alpn)) ++alpn; else { LSQ_ERROR("cannot add ALPN %s", *alpn); exit(EXIT_FAILURE); } } if (0 != prog_prep(&prog)) { LSQ_ERROR("could not prep"); exit(EXIT_FAILURE); } LSQ_DEBUG("entering event loop"); s = prog_run(&prog); prog_cleanup(&prog); #if HAVE_REGEX if (!server_ctx.document_root) free_map_regexes(); #endif exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); }