mirror of
https://github.com/rkd77/elinks.git
synced 2025-02-02 15:09:23 -05:00
599 lines
13 KiB
C
599 lines
13 KiB
C
/* Parses and converts NNTP responses to enum values and cache entry HTML */
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "elinks.h"
|
|
|
|
#include "cache/cache.h"
|
|
#include "intl/gettext/libintl.h"
|
|
#include "mime/backend/common.h"
|
|
#include "network/connection.h"
|
|
#include "network/socket.h"
|
|
#include "protocol/header.h"
|
|
#include "protocol/nntp/codes.h"
|
|
#include "protocol/nntp/connection.h"
|
|
#include "protocol/nntp/nntp.h"
|
|
#include "protocol/nntp/response.h"
|
|
#include "protocol/protocol.h"
|
|
#include "protocol/uri.h"
|
|
#include "util/conv.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
|
|
|
|
/* Search for line ending \r\n pair */
|
|
static unsigned char *
|
|
get_nntp_line_end(unsigned char *data, int datalen)
|
|
{
|
|
for (; datalen > 1; data++, datalen--)
|
|
if (data[0] == ASCII_CR && data[1] == ASCII_LF)
|
|
return data + 2;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* RFC 977 - Section 2.4.1. Text Responses:
|
|
*
|
|
* A single line containing only a period (.) is sent to indicate the end of
|
|
* the text (i.e., the server will send a CR-LF pair at the end of the last
|
|
* line of text, a period, and another CR-LF pair).
|
|
*
|
|
* If the text contained a period as the first character of the text line in
|
|
* the original, that first period is doubled. Therefore, the client must
|
|
* examine the first character of each line received, and for those beginning
|
|
* with a period, determine either that this is the end of the text or whether
|
|
* to collapse the doubled period to a single one. */
|
|
/* Returns NULL if end-of-text is found else start of collapsed line */
|
|
static inline unsigned char *
|
|
check_nntp_line(unsigned char *line, unsigned char *end)
|
|
{
|
|
assert(line < end);
|
|
|
|
/* Just to be safe NUL terminate the line */
|
|
end[-2] = 0;
|
|
|
|
if (line[0] != '.') return line;
|
|
|
|
if (!line[1]) return NULL;
|
|
if (line[1] == '.') line++;
|
|
|
|
return line;
|
|
}
|
|
|
|
static inline unsigned char *
|
|
get_nntp_message_header_end(unsigned char *data, int datalen)
|
|
{
|
|
unsigned char *end, *prev_end = data;
|
|
|
|
while ((end = get_nntp_line_end(data, datalen))) {
|
|
datalen -= end - data;
|
|
data = end;
|
|
|
|
/* If only \r\n is there */
|
|
if (prev_end + 2 == end) {
|
|
/* NUL terminate the header so that it ends with just
|
|
* one \r\n usefull for appending to cached->head. */
|
|
end[-2] = 0;
|
|
return end;
|
|
}
|
|
|
|
prev_end = end;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct connection_state
|
|
init_nntp_header(struct connection *conn, struct read_buffer *rb)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
|
|
if (!conn->cached) {
|
|
conn->cached = get_cache_entry(conn->uri);
|
|
if (!conn->cached) return connection_state(S_OUT_OF_MEM);
|
|
|
|
} else if (conn->cached->head || conn->cached->content_type) {
|
|
/* If the head is set wipe out the content to be sure */
|
|
delete_entry_content(conn->cached);
|
|
mem_free_set(&conn->cached->head, NULL);
|
|
}
|
|
|
|
/* XXX: Override any Content-Type line in the header */
|
|
mem_free_set(&conn->cached->content_type, stracpy("text/html"));
|
|
if (!conn->cached->content_type)
|
|
return connection_state(S_OUT_OF_MEM);
|
|
|
|
switch (nntp->target) {
|
|
case NNTP_TARGET_ARTICLE_NUMBER:
|
|
case NNTP_TARGET_MESSAGE_ID:
|
|
case NNTP_TARGET_GROUP_MESSAGE_ID:
|
|
{
|
|
unsigned char *end;
|
|
|
|
end = get_nntp_message_header_end(rb->data, rb->length);
|
|
if (!end) {
|
|
/* Redo the whole cache entry thing next time */
|
|
return connection_state(S_TRANS);
|
|
}
|
|
|
|
/* FIXME: Add the NNTP response code line */
|
|
conn->cached->head = stracpy("FIXME NNTP response code\r\n");
|
|
if (!conn->cached->head) return connection_state(S_OUT_OF_MEM);
|
|
|
|
add_to_strn(&conn->cached->head, rb->data);
|
|
|
|
/* ... and remove it */
|
|
conn->received += end - rb->data;
|
|
kill_buffer_data(rb, end - rb->data);
|
|
break;
|
|
}
|
|
case NNTP_TARGET_ARTICLE_RANGE:
|
|
case NNTP_TARGET_GROUP:
|
|
case NNTP_TARGET_GROUPS:
|
|
case NNTP_TARGET_QUIT:
|
|
break;
|
|
}
|
|
|
|
return connection_state(S_OK);
|
|
}
|
|
|
|
|
|
static unsigned char *
|
|
get_nntp_title(struct connection *conn)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
struct string title;
|
|
|
|
if (!init_string(&title))
|
|
return NULL;
|
|
|
|
switch (nntp->target) {
|
|
case NNTP_TARGET_ARTICLE_RANGE:
|
|
add_format_to_string(&title, "Articles in the range %ld to %ld",
|
|
nntp->current_article, nntp->end_article);
|
|
break;
|
|
|
|
case NNTP_TARGET_ARTICLE_NUMBER:
|
|
case NNTP_TARGET_MESSAGE_ID:
|
|
case NNTP_TARGET_GROUP_MESSAGE_ID:
|
|
{
|
|
unsigned char *subject;
|
|
|
|
subject = parse_header(conn->cached->head, "Subject", NULL);
|
|
if (subject) {
|
|
add_to_string(&title, subject);
|
|
mem_free(subject);
|
|
break;
|
|
}
|
|
|
|
add_format_to_string(&title, "Article "),
|
|
add_string_to_string(&title, &nntp->message);
|
|
|
|
if (nntp->target == NNTP_TARGET_MESSAGE_ID)
|
|
break;
|
|
|
|
add_format_to_string(&title, " in ");
|
|
add_string_to_string(&title, &nntp->group);
|
|
break;
|
|
}
|
|
case NNTP_TARGET_GROUP:
|
|
add_format_to_string(&title, "Articles in "),
|
|
add_string_to_string(&title, &nntp->group);
|
|
break;
|
|
|
|
case NNTP_TARGET_GROUPS:
|
|
add_format_to_string(&title, "Newsgroups on "),
|
|
add_uri_to_string(&title, conn->uri, URI_PUBLIC);
|
|
break;
|
|
|
|
case NNTP_TARGET_QUIT:
|
|
break;
|
|
}
|
|
|
|
return title.source;
|
|
}
|
|
|
|
|
|
static void
|
|
decode_q_segment(struct string *str, unsigned char *in, unsigned char *end)
|
|
{
|
|
int c;
|
|
|
|
while ((c = *in++) != 0 && (in <= end)) {
|
|
if (c == '=') {
|
|
int d = *in++;
|
|
|
|
if (d == '\n' || !d)
|
|
break; /* drop trailing newline */
|
|
d = (unhx(d) << 4) | unhx(*in++);
|
|
add_format_to_string(str, "&#%d;", d);
|
|
continue;
|
|
}
|
|
|
|
if (c == '_') /* rfc2047 4.2 (2) */
|
|
c = 0x20;
|
|
add_char_to_string(str, c);
|
|
}
|
|
}
|
|
|
|
static void
|
|
decode_b_segment(struct string *str, unsigned char *in, unsigned char *end)
|
|
{
|
|
/* Decode in..ep, possibly in-place to ot */
|
|
int c, pos = 0, acc = 0;
|
|
|
|
while ((c = *in++) != 0 && (in <= end)) {
|
|
if (c == '+')
|
|
c = 62;
|
|
else if (c == '/')
|
|
c = 63;
|
|
else if ('A' <= c && c <= 'Z')
|
|
c -= 'A';
|
|
else if ('a' <= c && c <= 'z')
|
|
c -= 'a' - 26;
|
|
else if ('0' <= c && c <= '9')
|
|
c -= '0' - 52;
|
|
else if (c == '=') {
|
|
/* padding is almost like (c == 0), except we do
|
|
* not output NUL resulting only from it;
|
|
* for now we just trust the data.
|
|
*/
|
|
c = 0;
|
|
}
|
|
else
|
|
continue; /* garbage */
|
|
|
|
switch (pos++) {
|
|
case 0:
|
|
acc = (c << 2);
|
|
break;
|
|
case 1:
|
|
add_format_to_string(str, "&#%d;", (acc | (c >> 4)));
|
|
acc = (c & 15) << 4;
|
|
break;
|
|
case 2:
|
|
add_format_to_string(str, "&#%d;", (acc | (c >> 2)));
|
|
acc = (c & 3) << 6;
|
|
break;
|
|
case 3:
|
|
add_format_to_string(str, "&#%d;", (acc | c));
|
|
acc = pos = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
add_header_to_string(struct string *str, unsigned char *header)
|
|
{
|
|
unsigned char *end;
|
|
int rfc2047 = 0;
|
|
|
|
while ((end = strstr(header, "=?")) != NULL) {
|
|
int encoding;
|
|
unsigned char charset_q[256];
|
|
unsigned char *cp, *sp;
|
|
|
|
rfc2047 = 1;
|
|
|
|
if (header != end) {
|
|
add_html_to_string(str, header, end - header);
|
|
header = end;
|
|
}
|
|
|
|
/* E.g.
|
|
* ep : "=?iso-2022-jp?B?GyR...?= foo"
|
|
* ep : "=?ISO-8859-1?Q?Foo=FCbar?= baz"
|
|
*/
|
|
end += 2;
|
|
cp = strchr(end, '?');
|
|
if (!cp)
|
|
break;
|
|
|
|
for (sp = end; sp < cp; sp++)
|
|
charset_q[sp - end] = c_tolower(*sp);
|
|
charset_q[cp - end] = 0;
|
|
encoding = c_tolower(cp[1]);
|
|
|
|
if (!encoding || cp[2] != '?')
|
|
break;
|
|
cp += 3;
|
|
end = strstr(cp + 3, "?=");
|
|
if (!end)
|
|
break;
|
|
if (encoding == 'b')
|
|
decode_b_segment(str, cp, end);
|
|
else if (encoding == 'q')
|
|
decode_q_segment(str, cp, end);
|
|
else
|
|
break;
|
|
|
|
header = end + 2;
|
|
}
|
|
|
|
add_html_to_string(str, header, strlen(header));
|
|
}
|
|
|
|
|
|
static void
|
|
add_nntp_html_start(struct string *html, struct connection *conn)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
unsigned char *title = get_nntp_title(conn);
|
|
|
|
add_format_to_string(html,
|
|
"<html>\n"
|
|
"<head><title>%s</title></head>\n"
|
|
"<body>\n",
|
|
empty_string_or_(title));
|
|
|
|
switch (nntp->target) {
|
|
case NNTP_TARGET_ARTICLE_NUMBER:
|
|
case NNTP_TARGET_MESSAGE_ID:
|
|
case NNTP_TARGET_GROUP_MESSAGE_ID:
|
|
{
|
|
unsigned char *header_entries;
|
|
|
|
header_entries = get_nntp_header_entries();
|
|
if (!*header_entries) break;
|
|
|
|
add_to_string(html, "<pre>");
|
|
|
|
while (*header_entries) {
|
|
unsigned char *entry, *value;
|
|
|
|
entry = get_next_path_filename(&header_entries, ',');
|
|
if (!entry) continue;
|
|
|
|
value = parse_header(conn->cached->head, entry, NULL);
|
|
if (!value) {
|
|
mem_free(entry);
|
|
continue;
|
|
}
|
|
|
|
add_format_to_string(html, "<b>%s</b>: ", entry);
|
|
add_header_to_string(html, value);
|
|
add_char_to_string(html, '\n');
|
|
mem_free(value);
|
|
mem_free(entry);
|
|
}
|
|
|
|
add_to_string(html, "<hr />");
|
|
break;
|
|
}
|
|
case NNTP_TARGET_ARTICLE_RANGE:
|
|
case NNTP_TARGET_GROUP:
|
|
case NNTP_TARGET_GROUPS:
|
|
add_format_to_string(html,
|
|
"<h2>%s</h2>\n"
|
|
"<hr />\n"
|
|
"<ol>",
|
|
empty_string_or_(title));
|
|
break;
|
|
|
|
case NNTP_TARGET_QUIT:
|
|
break;
|
|
}
|
|
|
|
mem_free_if(title);
|
|
}
|
|
|
|
static void
|
|
add_nntp_html_end(struct string *html, struct connection *conn)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
|
|
switch (nntp->target) {
|
|
case NNTP_TARGET_ARTICLE_NUMBER:
|
|
case NNTP_TARGET_MESSAGE_ID:
|
|
case NNTP_TARGET_GROUP_MESSAGE_ID:
|
|
add_to_string(html, "</pre>");
|
|
break;
|
|
|
|
case NNTP_TARGET_ARTICLE_RANGE:
|
|
case NNTP_TARGET_GROUP:
|
|
case NNTP_TARGET_GROUPS:
|
|
add_to_string(html, "</ol>");
|
|
break;
|
|
|
|
case NNTP_TARGET_QUIT:
|
|
break;
|
|
}
|
|
|
|
add_to_string(html, "\n<hr />\n</body>\n</html>");
|
|
}
|
|
|
|
static void
|
|
add_nntp_html_line(struct string *html, struct connection *conn,
|
|
unsigned char *line)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
|
|
switch (nntp->target) {
|
|
case NNTP_TARGET_ARTICLE_NUMBER:
|
|
case NNTP_TARGET_MESSAGE_ID:
|
|
case NNTP_TARGET_GROUP_MESSAGE_ID:
|
|
add_html_to_string(html, line, strlen(line));
|
|
break;
|
|
|
|
case NNTP_TARGET_ARTICLE_RANGE:
|
|
case NNTP_TARGET_GROUP:
|
|
case NNTP_TARGET_GROUPS:
|
|
{
|
|
unsigned char *field = line;
|
|
|
|
line = strchr(line, '\t');
|
|
if (!line)
|
|
field = "";
|
|
else
|
|
*line++ = 0;
|
|
add_format_to_string(html, "<li value=\"%s\"><a href=\"%s/%s\">",
|
|
field, struri(conn->uri), field);
|
|
|
|
field = line;
|
|
line = strchr(line, '\t');
|
|
if (line)
|
|
*line++ = 0;
|
|
|
|
add_header_to_string(html, field);
|
|
add_to_string(html, "</a> ");
|
|
|
|
if (line) {
|
|
field = line;
|
|
line = strchr(line, '\t');
|
|
if (line)
|
|
*line++ = 0;
|
|
|
|
add_header_to_string(html, field);
|
|
}
|
|
add_to_string(html, "</li>");
|
|
break;
|
|
}
|
|
case NNTP_TARGET_QUIT:
|
|
break;
|
|
}
|
|
|
|
add_char_to_string(html, '\n');
|
|
}
|
|
|
|
struct connection_state
|
|
read_nntp_response_data(struct connection *conn, struct read_buffer *rb)
|
|
{
|
|
struct string html;
|
|
unsigned char *end;
|
|
struct connection_state state = connection_state(S_TRANS);
|
|
|
|
if (conn->from == 0) {
|
|
switch (init_nntp_header(conn, rb).basic) {
|
|
case S_OK:
|
|
break;
|
|
|
|
case S_OUT_OF_MEM:
|
|
return connection_state(S_OUT_OF_MEM);
|
|
|
|
case S_TRANS:
|
|
return connection_state(S_TRANS);
|
|
|
|
default:
|
|
return connection_state(S_NNTP_ERROR);
|
|
}
|
|
}
|
|
|
|
if (!init_string(&html))
|
|
return connection_state(S_OUT_OF_MEM);
|
|
|
|
if (conn->from == 0)
|
|
add_nntp_html_start(&html, conn);
|
|
|
|
while ((end = get_nntp_line_end(rb->data, rb->length))) {
|
|
unsigned char *line = check_nntp_line(rb->data, end);
|
|
|
|
if (!line) {
|
|
state = connection_state(S_OK);
|
|
break;
|
|
}
|
|
|
|
add_nntp_html_line(&html, conn, line);
|
|
|
|
conn->received += end - rb->data;
|
|
kill_buffer_data(rb, end - rb->data);
|
|
}
|
|
|
|
if (!is_in_state(state, S_TRANS))
|
|
add_nntp_html_end(&html, conn);
|
|
|
|
add_fragment(conn->cached, conn->from, html.source, html.length);
|
|
|
|
conn->from += html.length;
|
|
done_string(&html);
|
|
|
|
return state;
|
|
}
|
|
|
|
/* Interpret response code parameters for code 211 - after GROUP command */
|
|
/* The syntax is: 211 <articles> <first-article> <last-article> <name> */
|
|
/* Returns 1 on success and 0 on failure */
|
|
static int
|
|
parse_nntp_group_parameters(struct nntp_connection_info *nntp,
|
|
unsigned char *pos, unsigned char *end)
|
|
{
|
|
errno = 0;
|
|
|
|
/* Get <articles> */
|
|
while (pos < end && !isdigit(*pos))
|
|
pos++;
|
|
|
|
nntp->articles = strtol(pos, (char **) &pos, 10);
|
|
if (errno || pos >= end || nntp->articles < 0)
|
|
return 0;
|
|
|
|
if (nntp->target == NNTP_TARGET_ARTICLE_RANGE)
|
|
return 1;
|
|
|
|
/* Get <first-article> */
|
|
while (pos < end && !isdigit(*pos))
|
|
pos++;
|
|
|
|
nntp->current_article = strtol(pos, (char **) &pos, 10);
|
|
if (errno || pos >= end || nntp->current_article < 0)
|
|
return 0;
|
|
|
|
/* Get <last-article> */
|
|
while (pos < end && !isdigit(*pos))
|
|
pos++;
|
|
|
|
nntp->end_article = strtol(pos, (char **) &pos, 10);
|
|
if (errno || pos >= end || nntp->end_article < 0)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
enum nntp_code
|
|
get_nntp_response_code(struct connection *conn, struct read_buffer *rb)
|
|
{
|
|
struct nntp_connection_info *nntp = conn->info;
|
|
unsigned char *line = rb->data;
|
|
unsigned char *end = get_nntp_line_end(rb->data, rb->length);
|
|
enum nntp_code code;
|
|
int linelen;
|
|
|
|
if (!end) return NNTP_CODE_NONE;
|
|
|
|
/* Just to be safe NUL terminate the line */
|
|
end[-1] = 0;
|
|
|
|
linelen = end - line;
|
|
|
|
if (linelen < sizeof("xxx\r\n") - 1
|
|
|| !isdigit(line[0])
|
|
|| !isdigit(line[1])
|
|
|| !isdigit(line[2])
|
|
|| isdigit(line[3]))
|
|
return NNTP_CODE_INVALID;
|
|
|
|
code = atoi(line);
|
|
|
|
if (!check_nntp_code_valid(code))
|
|
return NNTP_CODE_INVALID;
|
|
|
|
/* Only when listing all articles in group the parameters is needed */
|
|
if (code == NNTP_CODE_211_GROUP_SELECTED
|
|
&& nntp->target == NNTP_TARGET_GROUP
|
|
&& !parse_nntp_group_parameters(nntp, line + 4, end))
|
|
return NNTP_CODE_INVALID;
|
|
|
|
/* Remove the response line */
|
|
kill_buffer_data(rb, linelen);
|
|
|
|
conn->received += linelen;
|
|
|
|
return code;
|
|
}
|