Merge pull request #172 from KDAB/webserver-cleaned

This is the basic web server functionality.

http://code.google.com/p/phantomjs/issues/detail?id=115
1.4
Ariya Hidayat 2011-11-15 08:11:24 -08:00
commit fa4391d514
17 changed files with 5221 additions and 3 deletions

52
examples/server.js Normal file
View File

@ -0,0 +1,52 @@
var page = require('webpage').create();
var server = require('webserver').create();
var host, port;
if (phantom.args.length !== 1) {
console.log('Usage: server.js <some port>');
phantom.exit();
} else {
port = phantom.args[0];
var listening = server.listen(port, function (request, response) {
console.log("GOT HTTP REQUEST");
console.log("request.url = " + request.url);
console.log("request.queryString = " + request.queryString);
console.log("request.method = " + request.method);
console.log("request.httpVersion = " + request.httpVersion);
console.log("request.statusCode = " + request.statusCode);
console.log("request.isSSL = " + request.isSSL);
console.log("request.remoteIP = " + request.remoteIP);
console.log("request.remotePort = " + request.remotePort);
console.log("request.remoteUser = " + request.remoteUser);
console.log("request.headers = " + request.headers);
for(var i = 0; i < request.headers; ++i) {
console.log("request.headerName(" + i + ") = " + request.headerName(i));
console.log("request.headerValue(" + i + ") = " + request.headerValue(i));
}
// we set the headers here
response.statusCode = 200;
response.headers = {"Cache": "no-cache", "Content-Type": "text/html"};
// this is also possible:
response.setHeader("foo", "bar");
// now we write the body
// note: the headers above will now be sent implictly
response.writeBody("<html><head><title>YES!</title></head>");
// note: writeBody can be called multiple times
response.writeBody("<body><p>pretty cool :)</body></html>");
});
if (!listening) {
console.log("could not create web server listening on port " + port);
phantom.exit();
}
var url = "http://localhost:" + port + "/foo/bar.php?asdf=true";
console.log(url);
page.open(url, function (status) {
if (status !== 'success') {
console.log('FAIL to load the address');
} else {
console.log(page.content);
}
phantom.exit();
});
}

View File

@ -37,7 +37,7 @@ function require(name) {
var code, func, exports;
if (name === 'webpage' || name === 'fs') {
if (name === 'webpage' || name === 'fs' || name == 'webserver') {
code = phantom.loadModuleSource(name);
func = new Function("exports", "window", code);
exports = {};
@ -55,3 +55,4 @@ function require(name) {
// Legacy way to use WebPage
window.WebPage = require('webpage').create;
window.WebServer = require('webserver').create;

View File

@ -37,7 +37,7 @@ function require(name) {
var code, func, exports;
if (name === 'webpage' || name === 'fs') {
if (name === 'webpage' || name === 'fs' || name === 'webserver') {
code = phantom.loadModuleSource(name);
func = new Function("exports", "window", code);
exports = {};
@ -55,3 +55,4 @@ function require(name) {
// Legacy way to use WebPage
window.WebPage = require('webpage').create;
window.WebServer = require('webserver').create;

130
src/modules/webserver.js Normal file
View File

@ -0,0 +1,130 @@
/*jslint sloppy: true, nomen: true */
/*global exports:true,phantom:true */
/*
This file is part of the PhantomJS project from Ofi Labs.
Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>
Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com>
Copyright (C) 2011 James Roe <roejames12@hotmail.com>
Copyright (C) 2011 execjosh, http://execjosh.blogspot.com
Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
Author: Milian Wolff <milian.wolff@kdab.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
exports.create = function (opts) {
var server = phantom.createWebServer();
var handlers = {};
function checkType(o, type) {
return typeof o === type;
}
function isObject(o) {
return checkType(o, 'object');
}
function isUndefined(o) {
return checkType(o, 'undefined');
}
function isUndefinedOrNull(o) {
return isUndefined(o) || null === o;
}
function copyInto(target, source) {
if (target === source || isUndefinedOrNull(source)) {
return target;
}
target = target || {};
// Copy into objects only
if (isObject(target)) {
// Make sure source exists
source = source || {};
if (isObject(source)) {
var i, newTarget, newSource;
for (i in source) {
if (source.hasOwnProperty(i)) {
newTarget = target[i];
newSource = source[i];
if (newTarget && isObject(newSource)) {
// Deep copy
newTarget = copyInto(target[i], newSource);
} else {
newTarget = newSource;
}
if (!isUndefined(newTarget)) {
target[i] = newTarget;
}
}
}
} else {
target = source;
}
}
return target;
}
function defineSetter(handlerName, signalName) {
server.__defineSetter__(handlerName, function (f) {
if (handlers && typeof handlers[signalName] === 'function') {
try {
this[signalName].disconnect(handlers[signalName]);
} catch (e) {}
}
handlers[signalName] = f;
this[signalName].connect(handlers[signalName]);
});
}
// deep copy
//TODO: use this?
// server.settings = JSON.parse(JSON.stringify(phantom.defaultServerSettings));
defineSetter("onNewRequest", "newRequest");
server.listen = function (port, handler) {
if (arguments.length === 2 && typeof handler === 'function') {
this.onNewRequest = handler;
//TODO: settings?
return this.listenOnPort(port);
}
throw "Wrong use of WebServer#listen";
};
// Copy options into server
if (opts) {
server = copyInto(server, opts);
}
return server;
};

6
src/mongoose/ReadMe.txt Normal file
View File

@ -0,0 +1,6 @@
This project contains version 3.1 of the Mongoose web server project, as
found at http://code.google.com/p/mongoose.
It contains the code for version 3.1 as of 26-May-2011 (revision 0ca751520abf).
It contains an additional change in pthread_cond_broadcast() [~line 865] to
improve stability when running a debug build.

4154
src/mongoose/mongoose.c Normal file

File diff suppressed because it is too large Load Diff

224
src/mongoose/mongoose.h Normal file
View File

@ -0,0 +1,224 @@
// Copyright (c) 2004-2010 Sergey Lyubka
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef MONGOOSE_HEADER_INCLUDED
#define MONGOOSE_HEADER_INCLUDED
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
struct mg_context; // Handle for the HTTP service itself
struct mg_connection; // Handle for the individual connection
// This structure contains information about the HTTP request.
struct mg_request_info {
void *user_data; // User-defined pointer passed to mg_start()
char *request_method; // "GET", "POST", etc
char *uri; // URL-decoded URI
char *http_version; // E.g. "1.0", "1.1"
char *query_string; // \0 - terminated
char *remote_user; // Authenticated user
char *log_message; // Mongoose error log message
long remote_ip; // Client's IP address
int remote_port; // Client's port
int status_code; // HTTP reply status code
int is_ssl; // 1 if SSL-ed, 0 if not
int num_headers; // Number of headers
struct mg_header {
char *name; // HTTP header name
char *value; // HTTP header value
} http_headers[64]; // Maximum 64 headers
};
// Various events on which user-defined function is called by Mongoose.
enum mg_event {
MG_NEW_REQUEST, // New HTTP request has arrived from the client
MG_HTTP_ERROR, // HTTP error must be returned to the client
MG_EVENT_LOG, // Mongoose logs an event, request_info.log_message
MG_INIT_SSL // Mongoose initializes SSL. Instead of mg_connection *,
// SSL context is passed to the callback function.
};
// Prototype for the user-defined function. Mongoose calls this function
// on every event mentioned above.
//
// Parameters:
// event: which event has been triggered.
// conn: opaque connection handler. Could be used to read, write data to the
// client, etc. See functions below that accept "mg_connection *".
// request_info: Information about HTTP request.
//
// Return:
// If handler returns non-NULL, that means that handler has processed the
// request by sending appropriate HTTP reply to the client. Mongoose treats
// the request as served.
// If callback returns NULL, that means that callback has not processed
// the request. Handler must not send any data to the client in this case.
// Mongoose proceeds with request handling as if nothing happened.
typedef void * (*mg_callback_t)(enum mg_event event,
struct mg_connection *conn,
const struct mg_request_info *request_info);
// Start web server.
//
// Parameters:
// callback: user defined event handling function or NULL.
// options: NULL terminated list of option_name, option_value pairs that
// specify Mongoose configuration parameters.
//
// Example:
// const char *options[] = {
// "document_root", "/var/www",
// "listening_ports", "80,443s",
// NULL
// };
// struct mg_context *ctx = mg_start(&my_func, NULL, options);
//
// Please refer to http://code.google.com/p/mongoose/wiki/MongooseManual
// for the list of valid option and their possible values.
//
// Return:
// web server context, or NULL on error.
struct mg_context *mg_start(mg_callback_t callback, void *user_data,
const char **options);
// Stop the web server.
//
// Must be called last, when an application wants to stop the web server and
// release all associated resources. This function blocks until all Mongoose
// threads are stopped. Context pointer becomes invalid.
void mg_stop(struct mg_context *);
// Get the value of particular configuration parameter.
// The value returned is read-only. Mongoose does not allow changing
// configuration at run time.
// If given parameter name is not valid, NULL is returned. For valid
// names, return value is guaranteed to be non-NULL. If parameter is not
// set, zero-length string is returned.
const char *mg_get_option(const struct mg_context *ctx, const char *name);
// Return array of strings that represent valid configuration options.
// For each option, a short name, long name, and default value is returned.
// Array is NULL terminated.
const char **mg_get_valid_option_names(void);
// Add, edit or delete the entry in the passwords file.
//
// This function allows an application to manipulate .htpasswd files on the
// fly by adding, deleting and changing user records. This is one of the
// several ways of implementing authentication on the server side. For another,
// cookie-based way please refer to the examples/chat.c in the source tree.
//
// If password is not NULL, entry is added (or modified if already exists).
// If password is NULL, entry is deleted.
//
// Return:
// 1 on success, 0 on error.
int mg_modify_passwords_file(const char *passwords_file_name,
const char *domain,
const char *user,
const char *password);
// Send data to the client.
int mg_write(struct mg_connection *, const void *buf, size_t len);
// Send data to the browser using printf() semantics.
//
// Works exactly like mg_write(), but allows to do message formatting.
// Note that mg_printf() uses internal buffer of size IO_BUF_SIZE
// (8 Kb by default) as temporary message storage for formatting. Do not
// print data that is bigger than that, otherwise it will be truncated.
int mg_printf(struct mg_connection *, const char *fmt, ...);
// Read data from the remote end, return number of bytes read.
int mg_read(struct mg_connection *, void *buf, size_t len);
// Get the value of particular HTTP header.
//
// This is a helper function. It traverses request_info->http_headers array,
// and if the header is present in the array, returns its value. If it is
// not present, NULL is returned.
const char *mg_get_header(const struct mg_connection *, const char *name);
// Get a value of particular form variable.
//
// Parameters:
// data: pointer to form-uri-encoded buffer. This could be either POST data,
// or request_info.query_string.
// data_len: length of the encoded data.
// var_name: variable name to decode from the buffer
// buf: destination buffer for the decoded variable
// buf_len: length of the destination buffer
//
// Return:
// On success, length of the decoded variable.
// On error, -1 (variable not found, or destination buffer is too small).
//
// Destination buffer is guaranteed to be '\0' - terminated. In case of
// failure, dst[0] == '\0'.
int mg_get_var(const char *data, size_t data_len,
const char *var_name, char *buf, size_t buf_len);
// Fetch value of certain cookie variable into the destination buffer.
//
// Destination buffer is guaranteed to be '\0' - terminated. In case of
// failure, dst[0] == '\0'. Note that RFC allows many occurrences of the same
// parameter. This function returns only first occurrence.
//
// Return:
// On success, value length.
// On error, -1 (either "Cookie:" header is not present at all, or the
// requested parameter is not found, or destination buffer is too small
// to hold the value).
int mg_get_cookie(const struct mg_connection *,
const char *cookie_name, char *buf, size_t buf_len);
// Return Mongoose version.
const char *mg_version(void);
// MD5 hash given strings.
// Buffer 'buf' must be 33 bytes long. Varargs is a NULL terminated list of
// asciiz strings. When function returns, buf will contain human-readable
// MD5 hash. Example:
// char buf[33];
// mg_md5(buf, "aa", "bb", NULL);
void mg_md5(char *buf, ...);
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // MONGOOSE_HEADER_INCLUDED

View File

@ -0,0 +1,5 @@
VPATH += $$PWD
INCLUDEPATH += $$PWD
SOURCES += mongoose.c
HEADERS += mongoose.h

View File

@ -40,6 +40,7 @@
#include "terminal.h"
#include "utils.h"
#include "webpage.h"
#include "webserver.h"
// public:
@ -185,6 +186,16 @@ QObject *Phantom::createWebPage()
return page;
}
QObject* Phantom::createWebServer()
{
WebServer *server = new WebServer(this, &m_config);
m_servers.append(server);
///TODO:
// page->applySettings(m_defaultPageSettings);
// page->setLibraryPath(QFileInfo(m_config.scriptFile()).dir().absolutePath());
return server;
}
QObject *Phantom::createFilesystem()
{
if (!m_filesystem)
@ -215,6 +226,8 @@ void Phantom::exit(int code)
qDeleteAll(m_pages);
m_pages.clear();
m_page = 0;
qDeleteAll(m_servers);
m_servers.clear();
QApplication::instance()->exit(code);
}

View File

@ -34,6 +34,7 @@
#include <QtGui>
class WebPage;
class WebServer;
#include "csconverter.h"
#include "filesystem.h"
#include "encoding.h"
@ -71,6 +72,7 @@ public:
public slots:
QObject *createWebPage();
QObject *createWebServer();
QObject *createFilesystem();
QString loadModuleSource(const QString &name);
bool injectJs(const QString &jsFilePath);
@ -88,6 +90,7 @@ private:
QVariantMap m_defaultPageSettings;
FileSystem *m_filesystem;
QList<QPointer<WebPage> > m_pages;
QList<QPointer<WebServer> > m_servers;
Config m_config;
};

View File

@ -13,6 +13,7 @@ RESOURCES = phantomjs.qrc
HEADERS += csconverter.h \
phantom.h \
webpage.h \
webserver.h \
consts.h \
utils.h \
networkaccessmanager.h \
@ -21,8 +22,10 @@ HEADERS += csconverter.h \
terminal.h \
encoding.h \
config.h
SOURCES += phantom.cpp \
webpage.cpp \
webserver.cpp \
main.cpp \
csconverter.cpp \
utils.cpp \
@ -37,9 +40,11 @@ OTHER_FILES += usage.txt \
bootstrap.js \
configurator.js \
modules/fs.js \
modules/webpage.js
modules/webpage.js \
modules/webserver.js
include(gif/gif.pri)
include(mongoose/mongoose.pri)
win32: RC_FILE = phantomjs_win.rc
os2: RC_FILE = phantomjs_os2.rc

View File

@ -6,6 +6,7 @@
<file>bootstrap.js</file>
<file>configurator.js</file>
<file>modules/webpage.js</file>
<file>modules/webserver.js</file>
<file>modules/fs.js</file>
</qresource>
</RCC>

358
src/webserver.cpp Normal file
View File

@ -0,0 +1,358 @@
/*
This file is part of the PhantomJS project from Ofi Labs.
Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
Author: Milian Wolff <milian.wolff@kdab.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "webserver.h"
#include <QDebug>
#include "mongoose.h"
#include <QThread>
#include <QHostAddress>
static void *callback(mg_event event,
mg_connection *conn,
const mg_request_info *request_info)
{
WebServer* server = static_cast<WebServer*>(request_info->user_data);
// note: we use a blocking queued connection to always handle the request in the main thread
// TODO: check whether direct call works as well
bool handled = false;
Qt::ConnectionType connectionType = Qt::DirectConnection;
if (QThread::currentThread() != server->thread()) {
connectionType = Qt::BlockingQueuedConnection;
}
QMetaObject::invokeMethod(server, "handleRequest", connectionType,
Q_ARG(mg_event, event), Q_ARG(mg_connection*, conn),
Q_ARG(const mg_request_info*, request_info),
Q_ARG(bool*, &handled));
if (handled) {
// anything non-null... pretty ugly, why not simply a bool??
return server;
} else {
return 0;
}
}
WebServer::WebServer(QObject *parent, const Config *config)
: QObject(parent)
, m_config(m_config)
, m_ctx(0)
{
setObjectName("WebServer");
}
WebServer::~WebServer()
{
close();
}
bool WebServer::listenOnPort(const QString& port)
{
///TODO: listen on multiple ports?
close();
const char *options[] = {"listening_ports", qstrdup(qPrintable(port)), NULL};
///TODO: more options from m_config?
m_ctx = mg_start(&callback, this, options);
if (!m_ctx) {
return false;
}
m_port = port;
return true;
}
QString WebServer::port() const
{
return m_port;
}
void WebServer::close()
{
if (m_ctx) {
mg_stop(m_ctx);
m_ctx = 0;
m_port.clear();
}
}
void WebServer::handleRequest(mg_event event, mg_connection *conn, const mg_request_info *request,
bool *handled)
{
Q_ASSERT(QThread::currentThread() == thread());
if (event == MG_NEW_REQUEST) {
WebServerRequest requestObj(request);
WebServerResponse responseObj(conn);
emit newRequest(&requestObj, &responseObj);
*handled = true;
return;
}
*handled = false;
}
//BEGIN WebServerRequest
WebServerRequest::WebServerRequest(const mg_request_info *request)
: m_request(request)
{
}
QString WebServerRequest::method() const
{
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->request_method);
}
QString WebServerRequest::httpVersion() const
{
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->http_version);
}
int WebServerRequest::statusCode() const
{
return m_request->status_code;
}
bool WebServerRequest::isSSL() const
{
return m_request->is_ssl;
}
QString WebServerRequest::url() const
{
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->uri);
}
QString WebServerRequest::queryString() const
{
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->query_string);
}
QString WebServerRequest::remoteIP() const
{
return QHostAddress(m_request->remote_ip).toString();
}
int WebServerRequest::remotePort() const
{
return m_request->remote_port;
}
QString WebServerRequest::remoteUser() const
{
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->remote_user);
}
int WebServerRequest::headers() const
{
return m_request->num_headers;
}
QString WebServerRequest::headerName(int header) const
{
Q_ASSERT(header >= 0 && header < m_request->num_headers);
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->http_headers[header].name);
}
QString WebServerRequest::headerValue(int header) const
{
Q_ASSERT(header >= 0 && header < m_request->num_headers);
///TODO: encoding?!
return QString::fromLocal8Bit(m_request->http_headers[header].value);
}
//END WebServerRequest
//BEGIN WebServerResponse
WebServerResponse::WebServerResponse(mg_connection *conn)
: m_conn(conn)
, m_statusCode(200)
, m_headersSent(false)
{
}
const char* responseCodeString(int code)
{
// see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
switch (code) {
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 307:
return "Temporary Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Request Entity Too Large";
case 414:
return "Request-URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Requested Range not Satisfiable";
case 417:
return "Expectation Failed";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 306:
// unused: fallthrough
default:
return "";
}
}
void WebServerResponse::writeHeaders(int statusCode, const QVariantMap &headers)
{
///TODO: what is the best-practice error handling in javascript? exceptions?
Q_ASSERT(!m_headersSent);
m_headersSent = true;
mg_printf(m_conn, "HTTP/1.1 %d %s\r\n", m_statusCode, responseCodeString(m_statusCode));
QVariantMap::const_iterator it = headers.constBegin();
while(it != headers.constEnd()) {
mg_printf(m_conn, "%s: %s\r\n", qPrintable(it.key()), qPrintable(it.value().toString()));
++it;
}
mg_write(m_conn, "\r\n", 2);
}
void WebServerResponse::writeBody(const QString &body)
{
if (!m_headersSent) {
writeHeaders(m_statusCode, m_headers);
}
///TODO: encoding?!
const QByteArray data = body.toLocal8Bit();
mg_write(m_conn, data.constData(), data.size());
}
int WebServerResponse::statusCode() const
{
return m_statusCode;
}
void WebServerResponse::setStatusCode(int code)
{
///TODO: what is the best-practice error handling in javascript? exceptions?
Q_ASSERT(!m_headersSent);
m_statusCode = code;
}
QString WebServerResponse::header(const QString &name) const
{
return m_headers.value(name).toString();
}
void WebServerResponse::setHeader(const QString &name, const QString &value)
{
///TODO: what is the best-practice error handling in javascript? exceptions?
Q_ASSERT(!m_headersSent);
m_headers.insert(name, value);
}
QVariantMap WebServerResponse::headers() const
{
return m_headers;
}
void WebServerResponse::setHeaders(const QVariantMap &headers)
{
///TODO: what is the best-practice error handling in javascript? exceptions?
Q_ASSERT(!m_headersSent);
m_headers = headers;
}
//END WebServerResponse

177
src/webserver.h Normal file
View File

@ -0,0 +1,177 @@
/*
This file is part of the PhantomJS project from Ofi Labs.
Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
Author: Milian Wolff <milian.wolff@kdab.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WEBSERVER_H
#define WEBSERVER_H
#include <QObject>
#include <QVariantMap>
///TODO: is this ok, or should it be put into .cpp
/// can be done by introducing a WebServerPrivate *d;
#include "mongoose.h"
class Config;
class WebServerRequest;
class WebServerResponse;
/**
* Scriptable HTTP web server.
*
* see also: modules/webserver.js
*/
class WebServer : public QObject
{
Q_OBJECT
Q_PROPERTY(QString port READ port);
public:
WebServer(QObject *parent, const Config *config);
virtual ~WebServer();
public slots:
/**
* Start listening for incoming connections on port @p port.
*
* For each new request @c handleRequest() will be called which
* in turn emits @c newRequest() where appropriate.
*
* @return true if we can listen on @p port, false otherwise.
*
* WARNING: must not be the same name as in the javascript api...
*/
bool listenOnPort(const QString &port);
/**
* @return the port this server is listening on
* or an empty string if the server is closed.
*/
QString port() const;
/// Stop listening for incoming connections.
void close();
signals:
/// @p request is a WebServerRequest, @p response is a WebServerResponse
void newRequest(QObject *request, QObject *response);
private slots:
void handleRequest(mg_event event, mg_connection* conn, const mg_request_info* request,
bool* handled);
private:
Config *m_config;
mg_context *m_ctx;
QString m_port;
};
/**
* Incoming HTTP client request.
*/
class WebServerRequest : public QObject
{
Q_OBJECT
Q_PROPERTY(QString method READ method)
Q_PROPERTY(QString httpVersion READ httpVersion)
Q_PROPERTY(int statusCode READ statusCode)
Q_PROPERTY(bool isSSL READ isSSL)
Q_PROPERTY(QString url READ url)
Q_PROPERTY(QString queryString READ queryString)
Q_PROPERTY(QString remoteIP READ remoteIP)
Q_PROPERTY(int remotePort READ remotePort)
Q_PROPERTY(QString remoteUser READ remoteUser)
Q_PROPERTY(int headers READ headers)
public:
WebServerRequest(const mg_request_info *request);
public slots:
/// @return request method, i.e. Get/Post
QString method() const;
QString httpVersion() const;
int statusCode() const;
bool isSSL() const;
QString url() const;
QString queryString() const;
QString remoteIP() const;
int remotePort() const;
QString remoteUser() const;
//TODO: better javascript api that allows easy "for(i in headers)" iteration?
//see e.g.: http://www.qtcentre.org/threads/31298-QtScript-bindings-to-advanced-containers-%28QMap-QList-etc%29
int headers() const;
QString headerName(int header) const;
QString headerValue(int header) const;
private:
const mg_request_info *m_request;
};
/**
* Outgoing HTTP response to client.
*/
class WebServerResponse : public QObject
{
Q_OBJECT
Q_PROPERTY(int statusCode READ statusCode WRITE setStatusCode);
Q_PROPERTY(QVariantMap headers READ headers WRITE setHeaders);
public:
WebServerResponse(mg_connection *conn);
public slots:
/// send @p headers to client with status code @p statusCode
void writeHeaders(int statusCode, const QVariantMap &headers);
/// sends @p data to client and makes sure the headers are send beforehand
void writeBody(const QString &data);
/// get the currently set status code, 200 is the default
int statusCode() const;
/// set the status code to @p code
void setStatusCode(int code);
/// get the value of header @p name
QString header(const QString &name) const;
/// set the value of header @p name to @p value
void setHeader(const QString &name, const QString &value);
/// get all headers
QVariantMap headers() const;
/// set all headers
void setHeaders(const QVariantMap &headers);
private:
mg_connection *m_conn;
int m_statusCode;
QVariantMap m_headers;
bool m_headersSent;
};
#endif // WEBSERVER_H

View File

@ -4,6 +4,10 @@ describe("module loading using require", function() {
expect(typeof require('webpage')).toEqual('object');
});
it("should work for 'webserver' module", function() {
expect(typeof require('webserver')).toEqual('object');
});
it("should work for 'fs' module", function() {
expect(typeof require('fs')).toEqual('object');
});

View File

@ -58,6 +58,7 @@ var fs = require('fs');
phantom.injectJs("./phantom-spec.js");
phantom.injectJs("./module-spec.js");
phantom.injectJs("./webpage-spec.js");
phantom.injectJs("./webserver-spec.js");
phantom.injectJs("./fs-spec-01.js"); //< Filesystem Specs 01 (Basic)
phantom.injectJs("./fs-spec-02.js"); //< Filesystem Specs 02 (Attributes)
phantom.injectJs("./fs-spec-03.js"); //< Filesystem Specs 03 (Paths)

83
test/webserver-spec.js Normal file
View File

@ -0,0 +1,83 @@
describe("WebServer constructor", function() {
it("should exist in window", function() {
expect(window.hasOwnProperty('WebServer')).toBeTruthy();
});
it("should be a function", function() {
expect(typeof window.WebServer).toEqual('function');
});
});
function checkRequest(request, response) {
expect(typeof request).toEqual('object');
expectHasProperty(request, 'url');
expectHasProperty(request, 'queryString');
expectHasProperty(request, 'method');
expectHasProperty(request, 'httpVersion');
expectHasProperty(request, 'statusCode');
expectHasProperty(request, 'isSSL');
expectHasProperty(request, 'remoteIP');
expectHasProperty(request, 'remotePort');
expectHasProperty(request, 'remoteUser');
expectHasProperty(request, 'headers');
expectHasProperty(request, 'headerName');
expectHasProperty(request, 'headerValue');
expect(typeof response).toEqual('object');
expectHasProperty(response, 'statusCode');
expectHasProperty(response, 'headers');
expectHasFunction(response, 'setHeader');
expectHasFunction(response, 'writeBody');
}
describe("WebServer object", function() {
var server = new WebServer();
it("should be creatable", function() {
expect(typeof server).toEqual('object');
expect(server).toNotEqual(null);
});
it("should have objectName as 'WebServer'", function() {
expect(server.objectName).toEqual('WebServer');
});
expectHasProperty(server, 'port');
it("should have port as string", function() {
expect(typeof server.port).toEqual('string');
});
it("should not listen to any port by default", function() {
expect(server.port).toEqual("");
});
/* TODO:
expectHasProperty(page, 'settings');
it("should have non-empty settings", function() {
expect(page.settings).toNotEqual(null);
expect(page.settings).toNotEqual({});
});
*/
expectHasFunction(server, 'listenOnPort');
expectHasFunction(server, 'newRequest');
expectHasFunction(server, 'close');
it("should fail to listen to blocked ports", function() {
//NOTE: is this really blocked everywhere?
expect(server.listen(1, function(){})).toEqual(false);
expect(server.port).toEqual("");
});
it("should be able to listen to some port", function() {
//NOTE: this can fail if the port is already being listend on...
expect(server.listen(12345, checkRequest)).toEqual(true);
expect(server.port).toEqual("12345");
});
it("should handle requests", function() {
var page = require('webpage').create();
var url = "http://localhost:12345/foo/bar.php?asdf=true";
page.open(url, function (status) {
expect(status == 'success').toEqual(true);
});
});
});