mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-01 22:12:33 +07:00
326 lines
11 KiB
C
326 lines
11 KiB
C
#include "mongoose.h"
|
|
|
|
#define DEFAULT_URL "http://localhost:9000/"
|
|
#define DEFAULT_CA_PEM "ca.pem"
|
|
#define DEFAULT_TIMEOUT_MS 1500
|
|
#define DEFAULT_MAX_CONNS 5
|
|
|
|
struct page {
|
|
struct page *next;
|
|
struct mg_str url; // Page URL
|
|
struct mg_str body; // Page body
|
|
int status; // Page state or HTTP status
|
|
int loading; // Number of failed load attempts
|
|
};
|
|
enum { PAGE_PENDING = 0, PAGE_FAILED = -1, PAGE_LOADING = -2 };
|
|
|
|
struct link {
|
|
struct link *next;
|
|
struct mg_str url; // Link URL
|
|
struct page *page; // Page where the URL was found
|
|
int status; // 0 - unverified, 1 - ok, 2 - broken
|
|
};
|
|
enum { LINK_UNVERIFIED = 0, LINK_OK = 1, LINK_BROKEN = 2 };
|
|
|
|
struct conndata { // Connection-specific data stored in c->data
|
|
uint64_t expire; // Connect expiration time in milliseconds
|
|
struct page *page; // Page being fetched on this connection
|
|
};
|
|
|
|
struct crawler {
|
|
struct mg_str url; // URL to crawl
|
|
struct mg_mgr mgr; // Event manager
|
|
struct mg_str ca; // CA PEM file
|
|
uint64_t timeout_ms; // Connect timeout in milliseconds
|
|
struct link *ignores; // Ignored links
|
|
struct link *links; // List of links
|
|
struct page *pages; // List of pages
|
|
int max_pages; // Max pages to crawl
|
|
int num_pages; // Pages crawled
|
|
int exit_on_broken; // Exit immediately when a broken link is found
|
|
};
|
|
|
|
static void ev_handler(struct mg_connection *c, int ev, void *ev_data);
|
|
|
|
static void add_page(struct crawler *crawler, const char *url) {
|
|
struct page *page = calloc(1, sizeof(*page));
|
|
page->url = mg_strdup(mg_str(url));
|
|
page->next = crawler->pages;
|
|
crawler->pages = page;
|
|
crawler->num_pages++;
|
|
// MG_INFO(("Added page %.*s", page->url.len, page->url.buf));
|
|
}
|
|
|
|
static void add_link(struct link **head, struct mg_str url, struct page *page) {
|
|
struct link *link = calloc(1, sizeof(*link));
|
|
link->url = mg_strdup(url);
|
|
link->page = page;
|
|
link->next = *head;
|
|
*head = link;
|
|
}
|
|
|
|
static int count_links(struct link *head, int status) {
|
|
int count = 0;
|
|
while (head != NULL) {
|
|
if (head->status == status) count++;
|
|
head = head->next;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static struct link *find_link(struct link *head, struct mg_str url) {
|
|
while (head != NULL) {
|
|
if (mg_strcasecmp(head->url, url) == 0) return head;
|
|
head = head->next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct page *find_page(struct page *head, struct mg_str url) {
|
|
while (head != NULL) {
|
|
if (mg_strcasecmp(head->url, url) == 0) return head;
|
|
head = head->next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void process_link(struct crawler *crawler, struct page *page,
|
|
struct mg_str url) {
|
|
if (mg_match(url, mg_str("#://#"), NULL)) {
|
|
// MG_INFO((" REMOTE %.*s", url.len, url.buf));
|
|
} else {
|
|
char buf[8192], *p;
|
|
if (url.len > 0 && url.buf[0] == '/') {
|
|
// Absolute path, prepend scheme and host from website root
|
|
struct mg_str caps[10] = {0};
|
|
if (!mg_match(page->url, mg_str("#://*/#"), caps) &&
|
|
!mg_match(page->url, mg_str("#://*"), caps)) {
|
|
MG_ERROR(("BAD page URL: %.*s", page->url.len, page->url.buf));
|
|
abort();
|
|
}
|
|
mg_snprintf(buf, sizeof(buf), "%.*s://%.*s%.*s", caps[0].len, caps[0].buf,
|
|
caps[1].len, caps[1].buf, url.len, url.buf);
|
|
} else {
|
|
// Relative path, prepend page URL
|
|
mg_snprintf(buf, sizeof(buf), "%.*s%.*s", page->url.len, page->url.buf,
|
|
url.len, url.buf);
|
|
}
|
|
if (find_link(crawler->links, mg_str(buf)) == NULL) {
|
|
add_link(&crawler->links, mg_str(buf), page);
|
|
}
|
|
if ((p = strrchr(buf, '#')) != NULL) *p = '\0'; // Strip fragment
|
|
if (find_page(crawler->pages, mg_str(buf)) == NULL &&
|
|
(crawler->max_pages == 0 || crawler->num_pages < crawler->max_pages)) {
|
|
add_page(crawler, buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scan page body for id="frag" or name="frag" using mg_match scanning loops
|
|
static bool find_fragment_in_body(struct mg_str body, struct mg_str frag) {
|
|
struct mg_str b = body;
|
|
struct mg_str caps[10] = {0};
|
|
while (mg_match(b, mg_str("#id=\"#\"#"), caps) ||
|
|
mg_match(b, mg_str("#name=\"#\"#"), caps)) {
|
|
size_t advance = caps[2].buf - b.buf;
|
|
b.buf += advance, b.len -= advance;
|
|
if (mg_strcasecmp(caps[1], frag) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void verify_links(struct crawler *crawler) {
|
|
for (struct link *link = crawler->links; link != NULL; link = link->next) {
|
|
if (link->status > 0) continue;
|
|
// Split URL into page URL part and fragment part
|
|
struct mg_str url = link->url, frag = {NULL, 0};
|
|
for (size_t i = 0; i < link->url.len; i++) {
|
|
if (link->url.buf[i] == '#') {
|
|
url.len = i;
|
|
frag.buf = link->url.buf + i + 1;
|
|
frag.len = link->url.len - i - 1;
|
|
break;
|
|
}
|
|
}
|
|
struct page *page = find_page(crawler->pages, url);
|
|
if (page == NULL || page->status == PAGE_PENDING ||
|
|
page->status == PAGE_LOADING)
|
|
continue; // not yet crawled or external
|
|
if (page->status != 200) {
|
|
link->status = LINK_BROKEN; // page fetch failed -> broken
|
|
} else if (page->body.buf == NULL) {
|
|
MG_ERROR(("Page body is NULL for %.*s", page->url.len, page->url.buf));
|
|
abort(); // This should never happen, but just in case
|
|
} else if (frag.len == 0) {
|
|
link->status = LINK_OK; // no fragment, page exists -> ok
|
|
} else if (find_fragment_in_body(page->body, frag)) {
|
|
link->status = LINK_OK; // fragment anchor found in body -> ok
|
|
} else {
|
|
link->status = LINK_BROKEN; // fragment anchor missing -> broken
|
|
}
|
|
if (link->status == LINK_BROKEN) {
|
|
MG_INFO(("BROKEN %.*s on page %.*s %lu", link->url.len, link->url.buf,
|
|
link->page->url.len, link->page->url.buf, page->body.len));
|
|
// mg_file_write(&mg_fs_posix, "/tmp/x", link->page->body.buf,
|
|
// link->page->body.len);
|
|
if (crawler->exit_on_broken) exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool should_parse_body(struct mg_str root, struct mg_str url) {
|
|
struct mg_str prefix = mg_str_n(url.buf, root.len);
|
|
if (url.len < root.len) return false;
|
|
if (mg_strcasecmp(root, prefix) == 0) return true;
|
|
return false;
|
|
}
|
|
|
|
static void save_body(struct crawler *crawler, struct page *page,
|
|
struct mg_str body) {
|
|
page->body = mg_strdup(body);
|
|
if (should_parse_body(crawler->url, page->url)) {
|
|
struct mg_str caps[10];
|
|
struct mg_str b = page->body; // Re-init
|
|
while (mg_match(b, mg_str("#href=\"#\"#"), caps)) { // Process href=...
|
|
process_link(crawler, page, caps[1]);
|
|
size_t advance = caps[2].buf - b.buf;
|
|
b.buf += advance, b.len -= advance;
|
|
}
|
|
b = page->body; // Re-init
|
|
while (mg_match(b, mg_str("#src=\"#\"#"), caps)) { // Process src=...
|
|
process_link(crawler, page, caps[1]);
|
|
size_t advance = caps[2].buf - b.buf;
|
|
b.buf += advance, b.len -= advance;
|
|
}
|
|
}
|
|
verify_links(crawler);
|
|
}
|
|
|
|
static void fail_page(struct page *page) {
|
|
if (page->status != PAGE_LOADING) return;
|
|
page->loading++;
|
|
if (page->loading >= 3) {
|
|
page->status = PAGE_FAILED;
|
|
MG_INFO(("FAILED %.*s", page->url.len, page->url.buf));
|
|
} else {
|
|
page->status = PAGE_PENDING;
|
|
}
|
|
}
|
|
|
|
static void ev_handler(struct mg_connection *c, int ev, void *ev_data) {
|
|
struct crawler *crawler = (struct crawler *) c->fn_data;
|
|
struct conndata *cd = (struct conndata *) c->data;
|
|
|
|
if (ev == MG_EV_OPEN) {
|
|
// Connection created. Store connect expiration time in c->data
|
|
cd->expire = mg_millis() + crawler->timeout_ms;
|
|
} else if (ev == MG_EV_POLL) {
|
|
if (mg_millis() > cd->expire) {
|
|
mg_error(c, "Connect timeout");
|
|
}
|
|
} else if (ev == MG_EV_CONNECT) {
|
|
// Connected to server. Extract host name from URL
|
|
struct mg_str host = mg_url_host(cd->page->url.buf);
|
|
if (c->is_tls) {
|
|
struct mg_tls_opts opts = {.ca = crawler->ca, .name = host};
|
|
mg_tls_init(c, &opts);
|
|
}
|
|
mg_printf(c,
|
|
"%s %s HTTP/1.0\r\n"
|
|
"Host: %.*s\r\n"
|
|
"Content-Type: octet-stream\r\n"
|
|
"\r\n",
|
|
"GET", mg_url_uri(cd->page->url.buf), host.len, host.buf);
|
|
} else if (ev == MG_EV_HTTP_MSG) {
|
|
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
|
cd->page->status = atoi(hm->uri.buf);
|
|
if (cd->page->status == 200) {
|
|
save_body(crawler, cd->page, hm->body);
|
|
} else {
|
|
MG_ERROR(("HTTP error %d for page %.*s", cd->page->status,
|
|
cd->page->url.len, cd->page->url.buf));
|
|
fail_page(cd->page);
|
|
}
|
|
c->is_closing = 1;
|
|
} else if (ev == MG_EV_CLOSE) {
|
|
fail_page(cd->page);
|
|
} else if (ev == MG_EV_ERROR) {
|
|
fail_page(cd->page);
|
|
MG_ERROR(("%s, page: %.*s", ev_data, cd->page->url.len, cd->page->url.buf));
|
|
}
|
|
}
|
|
|
|
static bool schedule_pages(struct crawler *crawler) {
|
|
unsigned num_conns = 0;
|
|
bool busy = false;
|
|
for (struct page *page = crawler->pages; page != NULL; page = page->next) {
|
|
if (page->status == PAGE_LOADING) {
|
|
num_conns++;
|
|
busy = true;
|
|
continue;
|
|
}
|
|
if (page->status != PAGE_PENDING) continue;
|
|
busy = true;
|
|
if (num_conns >= DEFAULT_MAX_CONNS) continue;
|
|
struct mg_connection *c =
|
|
mg_http_connect(&crawler->mgr, page->url.buf, ev_handler, crawler);
|
|
if (c == NULL) {
|
|
page->loading++;
|
|
if (page->loading >= 3) {
|
|
page->status = PAGE_FAILED;
|
|
MG_INFO(("FAILED %.*s", page->url.len, page->url.buf));
|
|
}
|
|
continue;
|
|
}
|
|
num_conns++;
|
|
page->status = PAGE_LOADING;
|
|
struct conndata *cd = (struct conndata *) c->data;
|
|
cd->page = page;
|
|
}
|
|
return busy;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
struct mg_str ca = mg_str(DEFAULT_CA_PEM);
|
|
struct crawler crawler = {
|
|
.url = mg_str(DEFAULT_URL),
|
|
.timeout_ms = DEFAULT_TIMEOUT_MS,
|
|
};
|
|
|
|
// Parse command-line flags
|
|
for (int i = 1; i + 1 < argc; i++) {
|
|
if (strcmp(argv[i], "-ca") == 0) {
|
|
ca = mg_str(argv[++i]);
|
|
} else if (strcmp(argv[i], "-url") == 0) {
|
|
crawler.url = mg_str(argv[++i]);
|
|
} else if (strcmp(argv[i], "-exit") == 0) {
|
|
crawler.exit_on_broken = 1;
|
|
} else if (strcmp(argv[i], "-max") == 0) {
|
|
crawler.max_pages = atoi(argv[++i]);
|
|
} else {
|
|
fprintf(stderr,
|
|
"Usage: %s OPTIONS\n"
|
|
" -exit - exit on broken link immeditely\n"
|
|
" -ca PEM - TLS CA PEM file path, default: %s\n"
|
|
" -url URL - URL to fetch, default: %s\n"
|
|
" -max NUMBER - maximum pages to crawl, default: 0 (all)\n",
|
|
argv[0], DEFAULT_CA_PEM, DEFAULT_URL);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
crawler.ca = mg_file_read(&mg_fs_posix, ca.buf);
|
|
|
|
mg_mgr_init(&crawler.mgr);
|
|
mg_log_set(MG_LL_INFO);
|
|
|
|
add_page(&crawler, crawler.url.buf);
|
|
while (schedule_pages(&crawler)) {
|
|
mg_mgr_poll(&crawler.mgr, 50);
|
|
}
|
|
mg_mgr_free(&crawler.mgr);
|
|
MG_INFO(("Exiting, %d pages, %d broken links, %d OK links, %d pending links",
|
|
crawler.num_pages, count_links(crawler.links, LINK_BROKEN),
|
|
count_links(crawler.links, LINK_OK), count_links(crawler.links, 0)));
|
|
return 0;
|
|
}
|