From fa73c478e95649792403a65ba8d427964228551d Mon Sep 17 00:00:00 2001 From: Peter Thorson Date: Wed, 7 Sep 2011 20:27:29 -0500 Subject: [PATCH] initial commit --- .gitignore | 13 + Makefile | 169 +++++++ examples/chat_server/Makefile | 0 examples/chat_server/chat.cpp | 30 ++ examples/chat_server/chat.hpp | 85 ++++ examples/chat_server/chat_server.cpp | 41 ++ examples/echo_server/Makefile | 23 + examples/echo_server/echo.cpp | 16 + examples/echo_server/echo.hpp | 35 ++ examples/echo_server/echo_client.html | 94 ++++ examples/echo_server/echo_server | Bin 0 -> 291596 bytes examples/echo_server/echo_server.cpp | 43 ++ readme.txt | 97 ++++ src/base64/base64.cpp | 123 +++++ src/base64/base64.h | 4 + src/network_utilities.cpp | 26 + src/network_utilities.hpp | 17 + src/sha1/Makefile | 41 ++ src/sha1/Makefile.nt | 48 ++ src/sha1/license.txt | 14 + src/sha1/sha.cpp | 176 +++++++ src/sha1/sha1.cpp | 589 +++++++++++++++++++++++ src/sha1/sha1.h | 89 ++++ src/sha1/shacmp.cpp | 169 +++++++ src/sha1/shatest.cpp | 149 ++++++ src/websocket_connection_handler.hpp | 61 +++ src/websocket_frame.cpp | 340 +++++++++++++ src/websocket_frame.hpp | 113 +++++ src/websocket_server.cpp | 44 ++ src/websocket_server.hpp | 40 ++ src/websocket_session.cpp | 666 ++++++++++++++++++++++++++ src/websocket_session.hpp | 209 ++++++++ src/websocketpp.hpp | 6 + todo.txt | 11 + 34 files changed, 3581 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 examples/chat_server/Makefile create mode 100644 examples/chat_server/chat.cpp create mode 100644 examples/chat_server/chat.hpp create mode 100644 examples/chat_server/chat_server.cpp create mode 100644 examples/echo_server/Makefile create mode 100644 examples/echo_server/echo.cpp create mode 100644 examples/echo_server/echo.hpp create mode 100644 examples/echo_server/echo_client.html create mode 100755 examples/echo_server/echo_server create mode 100644 examples/echo_server/echo_server.cpp create mode 100644 readme.txt create mode 100644 src/base64/base64.cpp create mode 100644 src/base64/base64.h create mode 100644 src/network_utilities.cpp create mode 100644 src/network_utilities.hpp create mode 100755 src/sha1/Makefile create mode 100755 src/sha1/Makefile.nt create mode 100755 src/sha1/license.txt create mode 100755 src/sha1/sha.cpp create mode 100755 src/sha1/sha1.cpp create mode 100755 src/sha1/sha1.h create mode 100755 src/sha1/shacmp.cpp create mode 100755 src/sha1/shatest.cpp create mode 100644 src/websocket_connection_handler.hpp create mode 100644 src/websocket_frame.cpp create mode 100644 src/websocket_frame.hpp create mode 100644 src/websocket_server.cpp create mode 100644 src/websocket_server.hpp create mode 100644 src/websocket_session.cpp create mode 100644 src/websocket_session.hpp create mode 100644 src/websocketpp.hpp create mode 100644 todo.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0ce170c0c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store + +#vim stuff +*~ +*.swp + +*.o +*.so +*.a +*.dylib + +objs_shared/ +objs_static/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d29fe6ed34 --- /dev/null +++ b/Makefile @@ -0,0 +1,169 @@ +objects = websocket_session.o websocket_server.o websocket_frame.o \ + network_utilities.o sha1.o base64.o + +OS=$(shell uname) + +# Defaults +ifeq ($(OS), Darwin) + cxxflags_default = -c -O2 -DNDEBUG +else + cxxflags_default = -c -O2 -DNDEBUG +endif +cxxflags_small = -c +cxxflags_debug = -c -g +cxxflags_shared = -f$(PIC) +libname = libwebsocketpp +libname_hdr = websocketpp +libname_debug = $(libname)_dbg +suffix_shared = so +suffix_shared_darwin = dylib +suffix_static = a +major_version = 0 +minor_version = 1.0 +objdir = objs + +# Variables +prefix ?= /usr/local +exec_prefix ?= $(prefix) +libdir ?= lib +includedir ?= include +srcdir ?= src +CXX ?= c++ +AR ?= ar +PIC ?= PIC +BUILD_TYPE ?= "default" +SHARED ?= "1" + +# Internal Variables +inst_path = $(exec_prefix)/$(libdir) +include_path = $(prefix)/$(includedir) + +# BUILD_TYPE specific settings +ifeq ($(BUILD_TYPE), debug) + CXXFLAGS = $(cxxflags_debug) + libname := $(libname_debug) +else + CXXFLAGS ?= $(cxxflags_default) +endif + +# SHARED specific settings +ifeq ($(SHARED), 1) + ifeq ($(OS), Darwin) + libname_shared = $(libname).$(suffix_shared_darwin) + else + libname_shared = $(libname).$(suffix_shared) + endif + libname_shared_major_version = $(libname_shared).$(major_version) + lib_target = $(libname_shared_major_version).$(minor_version) + objdir := $(objdir)_shared + CXXFLAGS := $(CXXFLAGS) $(cxxflags_shared) +else + lib_target = $(libname).$(suffix_static) + objdir := $(objdir)_static +endif + +# Phony targets +.PHONY: all banner installdirs install install_headers clean uninstall \ + uninstall_headers + +# Targets +all: $(lib_target) + @echo "============================================================" + @echo "Done" + @echo "============================================================" + +banner: + @echo "============================================================" + @echo "libwebsocketpp version: "$(major_version).$(minor_version) "target: "$(target) "OS: "$(OS) + @echo "============================================================" + +installdirs: banner + mkdir -p $(objdir) + +# Libraries +ifeq ($(SHARED),1) +$(lib_target): banner installdirs $(addprefix $(objdir)/, $(objects)) + @echo "Link " + cd $(objdir) ; \ + if test "$(OS)" = "Darwin" ; then \ + $(CXX) -dynamiclib -lboost_system -Wl,-dylib_install_name -Wl,$(libname_shared_major_version) -o $@ $(objects) ; \ + else \ + $(CXX) -shared -lboost_system -Wl,-soname,$(libname_shared_major_version) -o $@ $(objects) ; \ + fi ; \ + mv -f $@ ../ + @echo "Link: Done" +else +$(lib_target): banner installdirs $(addprefix $(objdir)/, $(objects)) + @echo "Archive" + cd $(objdir) ; \ + $(AR) -cvq $@ $(objects) ; \ + mv -f $@ ../ + @echo "Archive: Done" +endif + +# Compile object files +$(objdir)/sha1.o: $(srcdir)/sha1/sha1.cpp + $(CXX) $< -o $@ $(CXXFLAGS) + +$(objdir)/base64.o: $(srcdir)/base64/base64.cpp + $(CXX) $< -o $@ $(CXXFLAGS) + +$(objdir)/%.o: $(srcdir)/%.cpp + $(CXX) $< -o $@ $(CXXFLAGS) + +ifeq ($(SHARED),1) +install: banner install_headers $(lib_target) + @echo "Install shared library" + cp -f ./$(lib_target) $(inst_path) + cd $(inst_path) ; \ + ln -sf $(lib_target) $(libname_shared_major_version) ; \ + ln -sf $(libname_shared_major_version) $(libname_shared) + ifneq ($(OS),Darwin) + ldconfig + endif + @echo "Install shared library: Done." +else +install: banner install_headers $(lib_target) + @echo "Install static library" + cp -f ./$(lib_target) $(inst_path) + @echo "Install static library: Done." +endif + +install_headers: banner + @echo "Install header files" + mkdir -p $(include_path)/$(libname_hdr) +# cp -f ./*.hpp $(include_path)/$(libname_hdr) + cp -f ./$(srcdir)/*.hpp $(include_path)/$(libname_hdr) + mkdir -p $(include_path)/$(libname_hdr)/base64 + cp -f ./$(srcdir)/base64/base64.h $(include_path)/$(libname_hdr)/base64 + mkdir -p $(include_path)/$(libname_hdr)/sha1 + cp -f ./$(srcdir)/sha1/sha1.h $(include_path)/$(libname_hdr)/sha1 + chmod -R a+r $(include_path)/$(libname_hdr) + find $(include_path)/$(libname_hdr) -type d -exec chmod a+x {} \; + @echo "Install header files: Done." + +clean: banner + @echo "Clean library and object folder" + rm -rf $(objdir) + rm -f $(lib_target) + @echo "Clean library and object folder: Done" + +ifeq ($(SHARED),1) +uninstall: banner uninstall_headers + @echo "Uninstall shared library" + rm -f $(inst_path)/$(libname_shared) + rm -f $(inst_path)/$(libname_shared_major_version) + rm -f $(inst_path)/$(lib_target) + ldconfig + @echo "Uninstall shared library: Done" +else +uninstall: banner uninstall_headers + @echo "Uninstall static library" + rm -f $(inst_path)/$(lib_target) + @echo "Uninstall static library: Done" +endif + +uninstall_headers: banner + @echo "Uninstall header files" + rm -rf $(include_path)/$(libname) + @echo "Uninstall header files: Done" diff --git a/examples/chat_server/Makefile b/examples/chat_server/Makefile new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/chat_server/chat.cpp b/examples/chat_server/chat.cpp new file mode 100644 index 0000000000..7553297655 --- /dev/null +++ b/examples/chat_server/chat.cpp @@ -0,0 +1,30 @@ +#include "chat.hpp" + +using websocketchat::chat_handler; + + +bool chat_handler::validate(websocketpp::session_ptr client) { + // We only know about the chat resource + if (client->get_request() != "/chat") { + client->set_http_error(404); + return false; + } + + // Require specific origin example + if (client->get_header("Sec-WebSocket-Origin") != "http://zaphoyd.com") { + client->set_http_error(403); + return false; + } + + return true; +} + + +void connect(session_ptr client) { + std::cout << "client " << client << " joined the lobby." << std::endl; + m_connections.insert(client); + + // send user list to all clients + // send signon message from server to all other clients. + +} diff --git a/examples/chat_server/chat.hpp b/examples/chat_server/chat.hpp new file mode 100644 index 0000000000..98047a014e --- /dev/null +++ b/examples/chat_server/chat.hpp @@ -0,0 +1,85 @@ +#ifndef CHAT_HPP +#define CHAT_HPP + +// com.zaphoyd.websocketpp.chat protocol +// +// client messages: +// msg [UTF8 text] +// +// server messages: +// {"type":"msg","sender":"","value":"" } +// {"type":"participants","value":[,...]} + +#include + +#include + +#include +#include +#include +#include + +namespace websocketchat { + +class chat_handler : public websocketpp::connection_handler { +public: + chat_handler() {} + virtual ~chat_handler() {} + + bool validate(websocketpp::session_ptr client); + + // add new connection to the lobby + void connect(session_ptr client) { + std::cout << "client " << client << " joined the lobby." << std::endl; + m_connections.insert(client); + + // send user list to all clients + // send signon message from server to all other clients. + + } + + // someone disconnected from the lobby, remove them + void disconnect(websocketpp::session_ptr client,const std::string &reason) { + std::set::iterator it = m_connections.find(client); + + if (it == m_connections.end()) { + // this client has already disconnected, we can ignore this. + // this happens during certain types of disconnect where there is a + // deliberate "soft" disconnection preceeding the "hard" socket read + // fail or disconnect ack message. + return; + } + + std::cout << "client " << client << " left the lobby." << std::endl; + + m_connections.erase(it); + + // send signoff message from server to all clients + // send user list to all remaining clients + } + + void message(websocketpp::session_ptr client,const std::string &msg) { + std::cout << "message from client " << client << ": " << msg << std::endl; + + // create JSON message to send based on msg + + for (std::set::iterator it = m_connections.begin(); + it != m_connections.end(); it++) { + (*it)->send(msg); + } + } + + // lobby will ignore binary messages + void message(websocketpp::session_ptr client, + const std::vector &data) {} +private: + std::string serialize_state(); + + // list of outstanding connections + std::set m_connections; +}; + +typedef boost::shared_ptr chat_handler_ptr; + +} +#endif // CHAT_HPP diff --git a/examples/chat_server/chat_server.cpp b/examples/chat_server/chat_server.cpp new file mode 100644 index 0000000000..cbce304b55 --- /dev/null +++ b/examples/chat_server/chat_server.cpp @@ -0,0 +1,41 @@ +#include "chat.hpp" + +#include +#include + +#include + +using boost::asio::ip::tcp; + +int main(int argc, char* argv[]) { + std::string host = "localhost:5000"; + short port = 5000; + + if (argc == 3) { + port = atoi(argv[2]); + + std::stringstream temp; + temp << argv[1] << ":" << port; + + host = temp.str(); + } + + websocketchat::chat_handler_ptr chat_handler(new websocketchat::chat_handler()); + + try { + boost::asio::io_service io_service; + tcp::endpoint endpoint(tcp::v6(), port); + + websocketpp::server_ptr server( + new websocketpp::server(io_service,endpoint,host,chat_handler) + ); + + std::cout << "Starting chat server on " << host << std::endl; + + io_service.run(); + } catch (std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + + return 0; +} diff --git a/examples/echo_server/Makefile b/examples/echo_server/Makefile new file mode 100644 index 0000000000..dcdfdeb85e --- /dev/null +++ b/examples/echo_server/Makefile @@ -0,0 +1,23 @@ +CFLAGS = -O2 +LDFLAGS = + +CXX ?= c++ +SHARED ?= "1" + +ifeq ($(SHARED), 1) + LDFLAGS := $(LDFLAGS) -lboost_system -lwebsocketpp +else + LDFLAGS := $(LDFLAGS) -lboost_system ../../libwebsocketpp.a +endif + +echo_server: echo_server.o echo.o + $(CXX) $(CFLAGS) $^ -o $@ $(LDFLAGS) + +%.o: %.cpp + $(CXX) -c $(CFLAGS) -o $@ $^ + +# cleanup by removing generated files +# +.PHONY: clean +clean: + rm -f *.o echo_server diff --git a/examples/echo_server/echo.cpp b/examples/echo_server/echo.cpp new file mode 100644 index 0000000000..ab677a0772 --- /dev/null +++ b/examples/echo_server/echo.cpp @@ -0,0 +1,16 @@ +#include "echo.hpp" + +using websocketecho::echo_handler; + +bool echo_handler::validate(websocketpp::session_ptr client) { + return true; +} + +void echo_handler::message(websocketpp::session_ptr client, const std::string &msg) { + client->send(msg); +} + +void echo_handler::message(websocketpp::session_ptr client, + const std::vector &data) { + client->send(data); +} diff --git a/examples/echo_server/echo.hpp b/examples/echo_server/echo.hpp new file mode 100644 index 0000000000..221f305ebb --- /dev/null +++ b/examples/echo_server/echo.hpp @@ -0,0 +1,35 @@ +#ifndef ECHO_HANDLER_HPP +#define ECHO_HANDLER_HPP + +#include "../../src/websocket_connection_handler.hpp" +#include + +#include +#include + +namespace websocketecho { + +class echo_handler : public websocketpp::connection_handler { +public: + echo_handler() {} + virtual ~echo_handler() {} + + // The echo server allows all domains is protocol free. + bool validate(websocketpp::session_ptr client); + + // an echo server is stateless. The handler has no need to keep track of connected + // clients. + void connect(websocketpp::session_ptr client) {} + void disconnect(websocketpp::session_ptr client,const std::string &reason) {} + + // both text and binary messages are echoed back to the sending client. + void message(websocketpp::session_ptr client,const std::string &msg); + void message(websocketpp::session_ptr client, + const std::vector &data); +}; + +typedef boost::shared_ptr echo_handler_ptr; + +} + +#endif // ECHO_HANDLER_HPP diff --git a/examples/echo_server/echo_client.html b/examples/echo_server/echo_client.html new file mode 100644 index 0000000000..8e6bc9bbce --- /dev/null +++ b/examples/echo_server/echo_client.html @@ -0,0 +1,94 @@ + + + + + + + + + + +
+
+ + +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/examples/echo_server/echo_server b/examples/echo_server/echo_server new file mode 100755 index 0000000000000000000000000000000000000000..51987c96093152755a388011721672928b9a62d5 GIT binary patch literal 291596 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%0AZC%!?O+^*gZRO5P?6lff7;dg> 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

+F95fU*O{DYi$h|? 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

H&|BqCKxe)jE6CDZH4QK6(t%8c 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 U`5>)wQgtO>HI}Pdz*fiq1DaZi5&!@I literal 0 HcmV?d00001 diff --git a/examples/echo_server/echo_server.cpp b/examples/echo_server/echo_server.cpp new file mode 100644 index 0000000000..f8200463e6 --- /dev/null +++ b/examples/echo_server/echo_server.cpp @@ -0,0 +1,43 @@ +#include "echo.hpp" + +#include "../../src/websocketpp.hpp" +#include + +#include + +using boost::asio::ip::tcp; + +int main(int argc, char* argv[]) { + std::string host = "localhost:5000"; + short port = 5000; + + if (argc == 3) { + // TODO: input validation? + port = atoi(argv[2]); + + + std::stringstream temp; + temp << argv[1] << ":" << port; + + host = temp.str(); + } + + websocketecho::echo_handler_ptr echo_handler(new websocketecho::echo_handler()); + + try { + boost::asio::io_service io_service; + tcp::endpoint endpoint(tcp::v6(), port); + + websocketpp::server_ptr server( + new websocketpp::server(io_service,endpoint,host,echo_handler) + ); + + std::cout << "Starting echo server on " << host << std::endl; + + io_service.run(); + } catch (std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + + return 0; +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000000..51331c39de --- /dev/null +++ b/readme.txt @@ -0,0 +1,97 @@ +How to use this library + +WebSocket++ is a C++ websocket server library implimented using the Boost Asio networking stack. It is designed to provide a simple interface + +Built on Asio's proactor asyncronious event loop. + +Building a program using WebSocket++ has two parts +1. Impliment a connection handler. +This is done by subclassing websocketpp::connection_handler. Each websocket connection is attached to a connection handler. The handler impliments the following methods: + +validate: +Called after the client handshake is recieved but before the connection is accepted. Allows cookie authentication, origin checking, subprotocol negotiation, etc + +connect: +Called when the connection has been established and writes are allowed. + +disconnect: +Called when the connection has been disconnected + +message: text and binary variants +Called when a new websocket message is recieved. + +The handler has access to the following websocket session api: +get_header +returns the value of an HTTP header sent by the client during the handshake + +get_request +returns the resource requested by the client in the handshake + +set_handler: +pass responsibility for this connection to another connection handler + +set_http_error: +reject the connection with a specific HTTP error + +add_header +adds an HTTP header to the server handshake + +set_subprotocol +selects a subprotocol for the connection + +send: text and binary varients +send a websocket message + +ping: +send a ping + +2. Start Asio's event loop with a TCP endpoint and your connection handler + +There are two example programs in the examples directory that demonstrate this use pattern. One is a trivial stateless echo server, the other is a simple web based chat client. Both include example javascript clients. The echo server is suitable for use with automated testing suites such as the Autobahn test suite. + +By default, a single connection handler object is used for all connections. If needs require, that default handler can either store per-connection state itself or create new handlers and pass off responsibility for the connection to them. + +How to build this library + +Build static library +make + +Build and install in system include directories +make install + +Avaliable flags: +- SHARED=1: build a shared instead of static library. +- DEBUG=1: build library with no optimizations, suitable for debugging. Debug library + is called libwebsocketpp_dbg +- CXX=*: uses * as the c++ compiler instead of system default + +Build tested on +- Mac OS X 10.7 with apple gcc 4.2, macports gcc 4.6, apple llvm/clang + + + + +Outstanding issues +- Acknowledgement details +- Add license information to each file +- Add license.txt file +- Subprotocol negotiation interface +- check draft 13 issues +- make website + +Unimplimented features +- SSL +- frame or streaming based api +- client features +- extension negotiation interface + +Acknowledgements +- Boost Asio and other libraries +- base64 library +- sha1 library +- htonll discussion +- build/makefile from libjson + +- Autobahn test suite +- testing by Keith Brisson + diff --git a/src/base64/base64.cpp b/src/base64/base64.cpp new file mode 100644 index 0000000000..071b05cd38 --- /dev/null +++ b/src/base64/base64.cpp @@ -0,0 +1,123 @@ +/* + base64.cpp and base64.h + + Copyright (C) 2004-2008 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +*/ + +#include "base64.h" +#include + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + +static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i <4) ; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) + { + for(j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while((i++ < 3)) + ret += '='; + + } + + return ret; + +} + +std::string base64_decode(std::string const& encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = base64_chars.find(char_array_4[i]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = i; j <4; j++) + char_array_4[j] = 0; + + for (j = 0; j <4; j++) + char_array_4[j] = base64_chars.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; +} \ No newline at end of file diff --git a/src/base64/base64.h b/src/base64/base64.h new file mode 100644 index 0000000000..ceb13579ce --- /dev/null +++ b/src/base64/base64.h @@ -0,0 +1,4 @@ +#include + +std::string base64_encode(unsigned char const* , unsigned int len); +std::string base64_decode(std::string const& s); \ No newline at end of file diff --git a/src/network_utilities.cpp b/src/network_utilities.cpp new file mode 100644 index 0000000000..9850a6de7c --- /dev/null +++ b/src/network_utilities.cpp @@ -0,0 +1,26 @@ +#include "network_utilities.hpp" + +uint64_t htonll(uint64_t src) { + static int typ = TYP_INIT; + unsigned char c; + union { + uint64_t ull; + unsigned char c[8]; + } x; + if (typ == TYP_INIT) { + x.ull = 0x01; + typ = (x.c[7] == 0x01ULL) ? TYP_BIGE : TYP_SMLE; + } + if (typ == TYP_BIGE) + return src; + x.ull = src; + c = x.c[0]; x.c[0] = x.c[7]; x.c[7] = c; + c = x.c[1]; x.c[1] = x.c[6]; x.c[6] = c; + c = x.c[2]; x.c[2] = x.c[5]; x.c[5] = c; + c = x.c[3]; x.c[3] = x.c[4]; x.c[4] = c; + return x.ull; +} + +uint64_t ntohll(uint64_t src) { + return htonll(src); +} \ No newline at end of file diff --git a/src/network_utilities.hpp b/src/network_utilities.hpp new file mode 100644 index 0000000000..154e1c5ad8 --- /dev/null +++ b/src/network_utilities.hpp @@ -0,0 +1,17 @@ +#ifndef NETWORK_UTILITIES_HPP +#define NETWORK_UTILITIES_HPP + +#include + +// http://www.viva64.com/en/k/0018/ +// TODO: impliment stuff from here: +// http://stackoverflow.com/questions/809902/64-bit-ntohl-in-c + +#define TYP_INIT 0 +#define TYP_SMLE 1 +#define TYP_BIGE 2 + +uint64_t htonll(uint64_t src); +uint64_t ntohll(uint64_t src); + +#endif // NETWORK_UTILITIES_HPP \ No newline at end of file diff --git a/src/sha1/Makefile b/src/sha1/Makefile new file mode 100755 index 0000000000..66aaf2b56f --- /dev/null +++ b/src/sha1/Makefile @@ -0,0 +1,41 @@ +# +# Makefile +# +# Copyright (C) 1998, 2009 +# Paul E. Jones +# All Rights Reserved. +# +############################################################################# +# $Id: Makefile 12 2009-06-22 19:34:25Z paulej $ +############################################################################# +# +# Description: +# This is a makefile for UNIX to build the programs sha, shacmp, and +# shatest +# +# + +CC = g++ + +CFLAGS = -c -O2 -Wall -D_FILE_OFFSET_BITS=64 + +LIBS = + +OBJS = sha1.o + +all: sha shacmp shatest + +sha: sha.o $(OBJS) + $(CC) -o $@ sha.o $(OBJS) $(LIBS) + +shacmp: shacmp.o $(OBJS) + $(CC) -o $@ shacmp.o $(OBJS) $(LIBS) + +shatest: shatest.o $(OBJS) + $(CC) -o $@ shatest.o $(OBJS) $(LIBS) + +%.o: %.cpp + $(CC) $(CFLAGS) -o $@ $< + +clean: + $(RM) *.o sha shacmp shatest diff --git a/src/sha1/Makefile.nt b/src/sha1/Makefile.nt new file mode 100755 index 0000000000..9bacf64a1d --- /dev/null +++ b/src/sha1/Makefile.nt @@ -0,0 +1,48 @@ +# +# Makefile.nt +# +# Copyright (C) 1998, 2009 +# Paul E. Jones +# All Rights Reserved. +# +############################################################################# +# $Id: Makefile.nt 13 2009-06-22 20:20:32Z paulej $ +############################################################################# +# +# Description: +# This is a makefile for Win32 to build the programs sha, shacmp, and +# shatest +# +# Portability Issues: +# Designed to work with Visual C++ +# +# + +.silent: + +!include + +RM = del /q + +LIBS = $(conlibs) setargv.obj + +CFLAGS = -D _CRT_SECURE_NO_WARNINGS /EHsc /O2 /W3 + +OBJS = sha1.obj + +all: sha.exe shacmp.exe shatest.exe + +sha.exe: sha.obj $(OBJS) + $(link) $(conflags) -out:$@ sha.obj $(OBJS) $(LIBS) + +shacmp.exe: shacmp.obj $(OBJS) + $(link) $(conflags) -out:$@ shacmp.obj $(OBJS) $(LIBS) + +shatest.exe: shatest.obj $(OBJS) + $(link) $(conflags) -out:$@ shatest.obj $(OBJS) $(LIBS) + +.cpp.obj: + $(cc) $(CFLAGS) $(cflags) $(cvars) $< + +clean: + $(RM) *.obj sha.exe shacmp.exe shatest.exe diff --git a/src/sha1/license.txt b/src/sha1/license.txt new file mode 100755 index 0000000000..8d7f3941cf --- /dev/null +++ b/src/sha1/license.txt @@ -0,0 +1,14 @@ +Copyright (C) 1998, 2009 +Paul E. Jones + +Freeware Public License (FPL) + +This software is licensed as "freeware." Permission to distribute +this software in source and binary forms, including incorporation +into other products, is hereby granted without a fee. THIS SOFTWARE +IS PROVIDED 'AS IS' AND WITHOUT ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHOR SHALL NOT BE HELD +LIABLE FOR ANY DAMAGES RESULTING FROM THE USE OF THIS SOFTWARE, EITHER +DIRECTLY OR INDIRECTLY, INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA +OR DATA BEING RENDERED INACCURATE. diff --git a/src/sha1/sha.cpp b/src/sha1/sha.cpp new file mode 100755 index 0000000000..ad860863af --- /dev/null +++ b/src/sha1/sha.cpp @@ -0,0 +1,176 @@ +/* + * sha.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved + * + ***************************************************************************** + * $Id: sha.cpp 13 2009-06-22 20:20:32Z paulej $ + ***************************************************************************** + * + * Description: + * This utility will display the message digest (fingerprint) for + * the specified file(s). + * + * Portability Issues: + * None. + */ + +#include +#include +#ifdef WIN32 +#include +#endif +#include +#include "sha1.h" + +/* + * Function prototype + */ +void usage(); + + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * argc: [in] + * This is the count of arguments in the argv array + * argv: [in] + * This is an array of filenames for which to compute message digests + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main(int argc, char *argv[]) +{ + SHA1 sha; // SHA-1 class + FILE *fp; // File pointer for reading files + char c; // Character read from file + unsigned message_digest[5]; // Message digest from "sha" + int i; // Counter + bool reading_stdin; // Are we reading standard in? + bool read_stdin = false; // Have we read stdin? + + /* + * Check the program arguments and print usage information if -? + * or --help is passed as the first argument. + */ + if (argc > 1 && (!strcmp(argv[1],"-?") || !strcmp(argv[1],"--help"))) + { + usage(); + return 1; + } + + /* + * For each filename passed in on the command line, calculate the + * SHA-1 value and display it. + */ + for(i = 0; i < argc; i++) + { + /* + * We start the counter at 0 to guarantee entry into the for loop. + * So if 'i' is zero, we will increment it now. If there is no + * argv[1], we will use STDIN below. + */ + if (i == 0) + { + i++; + } + + if (argc == 1 || !strcmp(argv[i],"-")) + { +#ifdef WIN32 + _setmode(_fileno(stdin), _O_BINARY); +#endif + fp = stdin; + reading_stdin = true; + } + else + { + if (!(fp = fopen(argv[i],"rb"))) + { + fprintf(stderr, "sha: unable to open file %s\n", argv[i]); + return 2; + } + reading_stdin = false; + } + + /* + * We do not want to read STDIN multiple times + */ + if (reading_stdin) + { + if (read_stdin) + { + continue; + } + + read_stdin = true; + } + + /* + * Reset the SHA1 object and process input + */ + sha.Reset(); + + c = fgetc(fp); + while(!feof(fp)) + { + sha.Input(c); + c = fgetc(fp); + } + + if (!reading_stdin) + { + fclose(fp); + } + + if (!sha.Result(message_digest)) + { + fprintf(stderr,"sha: could not compute message digest for %s\n", + reading_stdin?"STDIN":argv[i]); + } + else + { + printf( "%08X %08X %08X %08X %08X - %s\n", + message_digest[0], + message_digest[1], + message_digest[2], + message_digest[3], + message_digest[4], + reading_stdin?"STDIN":argv[i]); + } + } + + return 0; +} + +/* + * usage + * + * Description: + * This function will display program usage information to the user. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void usage() +{ + printf("usage: sha [ ...]\n"); + printf("\tThis program will display the message digest (fingerprint)\n"); + printf("\tfor files using the Secure Hashing Algorithm (SHA-1).\n"); +} diff --git a/src/sha1/sha1.cpp b/src/sha1/sha1.cpp new file mode 100755 index 0000000000..fdcbdcc0b4 --- /dev/null +++ b/src/sha1/sha1.cpp @@ -0,0 +1,589 @@ +/* + * sha1.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved. + * + ***************************************************************************** + * $Id: sha1.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This class implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * The Secure Hashing Standard, which uses the Secure Hashing + * Algorithm (SHA), produces a 160-bit message digest for a + * given data stream. In theory, it is highly improbable that + * two messages will produce the same message digest. Therefore, + * this algorithm can serve as a means of providing a "fingerprint" + * for a message. + * + * Portability Issues: + * SHA-1 is defined in terms of 32-bit "words". This code was + * written with the expectation that the processor has at least + * a 32-bit machine word size. If the machine word size is larger, + * the code should still function properly. One caveat to that + * is that the input functions taking characters and character arrays + * assume that only 8 bits of information are stored in each character. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits long. + * Although SHA-1 allows a message digest to be generated for + * messages of any number of bits less than 2^64, this implementation + * only works with messages with a length that is a multiple of 8 + * bits. + * + */ + + +#include "sha1.h" + +/* + * SHA1 + * + * Description: + * This is the constructor for the sha1 class. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +SHA1::SHA1() +{ + Reset(); +} + +/* + * ~SHA1 + * + * Description: + * This is the destructor for the sha1 class + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +SHA1::~SHA1() +{ + // The destructor does nothing +} + +/* + * Reset + * + * Description: + * This function will initialize the sha1 class member variables + * in preparation for computing a new message digest. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Reset() +{ + Length_Low = 0; + Length_High = 0; + Message_Block_Index = 0; + + H[0] = 0x67452301; + H[1] = 0xEFCDAB89; + H[2] = 0x98BADCFE; + H[3] = 0x10325476; + H[4] = 0xC3D2E1F0; + + Computed = false; + Corrupted = false; +} + +/* + * Result + * + * Description: + * This function will return the 160-bit message digest into the + * array provided. + * + * Parameters: + * message_digest_array: [out] + * This is an array of five unsigned integers which will be filled + * with the message digest that has been computed. + * + * Returns: + * True if successful, false if it failed. + * + * Comments: + * + */ +bool SHA1::Result(unsigned *message_digest_array) +{ + int i; // Counter + + if (Corrupted) + { + return false; + } + + if (!Computed) + { + PadMessage(); + Computed = true; + } + + for(i = 0; i < 5; i++) + { + message_digest_array[i] = H[i]; + } + + return true; +} + +/* + * Input + * + * Description: + * This function accepts an array of octets as the next portion of + * the message. + * + * Parameters: + * message_array: [in] + * An array of characters representing the next portion of the + * message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input( const unsigned char *message_array, + unsigned length) +{ + if (!length) + { + return; + } + + if (Computed || Corrupted) + { + Corrupted = true; + return; + } + + while(length-- && !Corrupted) + { + Message_Block[Message_Block_Index++] = (*message_array & 0xFF); + + Length_Low += 8; + Length_Low &= 0xFFFFFFFF; // Force it to 32 bits + if (Length_Low == 0) + { + Length_High++; + Length_High &= 0xFFFFFFFF; // Force it to 32 bits + if (Length_High == 0) + { + Corrupted = true; // Message is too long + } + } + + if (Message_Block_Index == 64) + { + ProcessMessageBlock(); + } + + message_array++; + } +} + +/* + * Input + * + * Description: + * This function accepts an array of octets as the next portion of + * the message. + * + * Parameters: + * message_array: [in] + * An array of characters representing the next portion of the + * message. + * length: [in] + * The length of the message_array + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input( const char *message_array, + unsigned length) +{ + Input((unsigned char *) message_array, length); +} + +/* + * Input + * + * Description: + * This function accepts a single octets as the next message element. + * + * Parameters: + * message_element: [in] + * The next octet in the message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input(unsigned char message_element) +{ + Input(&message_element, 1); +} + +/* + * Input + * + * Description: + * This function accepts a single octet as the next message element. + * + * Parameters: + * message_element: [in] + * The next octet in the message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input(char message_element) +{ + Input((unsigned char *) &message_element, 1); +} + +/* + * operator<< + * + * Description: + * This operator makes it convenient to provide character strings to + * the SHA1 object for processing. + * + * Parameters: + * message_array: [in] + * The character array to take as input. + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * Each character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const char *message_array) +{ + const char *p = message_array; + + while(*p) + { + Input(*p); + p++; + } + + return *this; +} + +/* + * operator<< + * + * Description: + * This operator makes it convenient to provide character strings to + * the SHA1 object for processing. + * + * Parameters: + * message_array: [in] + * The character array to take as input. + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * Each character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const unsigned char *message_array) +{ + const unsigned char *p = message_array; + + while(*p) + { + Input(*p); + p++; + } + + return *this; +} + +/* + * operator<< + * + * Description: + * This function provides the next octet in the message. + * + * Parameters: + * message_element: [in] + * The next octet in the message + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * The character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const char message_element) +{ + Input((unsigned char *) &message_element, 1); + + return *this; +} + +/* + * operator<< + * + * Description: + * This function provides the next octet in the message. + * + * Parameters: + * message_element: [in] + * The next octet in the message + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * The character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const unsigned char message_element) +{ + Input(&message_element, 1); + + return *this; +} + +/* + * ProcessMessageBlock + * + * Description: + * This function will process the next 512 bits of the message + * stored in the Message_Block array. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this function, especially the single + * character names, were used because those were the names used + * in the publication. + * + */ +void SHA1::ProcessMessageBlock() +{ + const unsigned K[] = { // Constants defined for SHA-1 + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + int t; // Loop counter + unsigned temp; // Temporary word value + unsigned W[80]; // Word sequence + unsigned A, B, C, D, E; // Word buffers + + /* + * Initialize the first 16 words in the array W + */ + for(t = 0; t < 16; t++) + { + W[t] = ((unsigned) Message_Block[t * 4]) << 24; + W[t] |= ((unsigned) Message_Block[t * 4 + 1]) << 16; + W[t] |= ((unsigned) Message_Block[t * 4 + 2]) << 8; + W[t] |= ((unsigned) Message_Block[t * 4 + 3]); + } + + for(t = 16; t < 80; t++) + { + W[t] = CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]); + } + + A = H[0]; + B = H[1]; + C = H[2]; + D = H[3]; + E = H[4]; + + for(t = 0; t < 20; t++) + { + temp = CircularShift(5,A) + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 20; t < 40; t++) + { + temp = CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 40; t < 60; t++) + { + temp = CircularShift(5,A) + + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 60; t < 80; t++) + { + temp = CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + H[0] = (H[0] + A) & 0xFFFFFFFF; + H[1] = (H[1] + B) & 0xFFFFFFFF; + H[2] = (H[2] + C) & 0xFFFFFFFF; + H[3] = (H[3] + D) & 0xFFFFFFFF; + H[4] = (H[4] + E) & 0xFFFFFFFF; + + Message_Block_Index = 0; +} + +/* + * PadMessage + * + * Description: + * According to the standard, the message must be padded to an even + * 512 bits. The first padding bit must be a '1'. The last 64 bits + * represent the length of the original message. All bits in between + * should be 0. This function will pad the message according to those + * rules by filling the message_block array accordingly. It will also + * call ProcessMessageBlock() appropriately. When it returns, it + * can be assumed that the message digest has been computed. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::PadMessage() +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second block. + */ + if (Message_Block_Index > 55) + { + Message_Block[Message_Block_Index++] = 0x80; + while(Message_Block_Index < 64) + { + Message_Block[Message_Block_Index++] = 0; + } + + ProcessMessageBlock(); + + while(Message_Block_Index < 56) + { + Message_Block[Message_Block_Index++] = 0; + } + } + else + { + Message_Block[Message_Block_Index++] = 0x80; + while(Message_Block_Index < 56) + { + Message_Block[Message_Block_Index++] = 0; + } + + } + + /* + * Store the message length as the last 8 octets + */ + Message_Block[56] = (Length_High >> 24) & 0xFF; + Message_Block[57] = (Length_High >> 16) & 0xFF; + Message_Block[58] = (Length_High >> 8) & 0xFF; + Message_Block[59] = (Length_High) & 0xFF; + Message_Block[60] = (Length_Low >> 24) & 0xFF; + Message_Block[61] = (Length_Low >> 16) & 0xFF; + Message_Block[62] = (Length_Low >> 8) & 0xFF; + Message_Block[63] = (Length_Low) & 0xFF; + + ProcessMessageBlock(); +} + + +/* + * CircularShift + * + * Description: + * This member function will perform a circular shifting operation. + * + * Parameters: + * bits: [in] + * The number of bits to shift (1-31) + * word: [in] + * The value to shift (assumes a 32-bit integer) + * + * Returns: + * The shifted value. + * + * Comments: + * + */ +unsigned SHA1::CircularShift(int bits, unsigned word) +{ + return ((word << bits) & 0xFFFFFFFF) | ((word & 0xFFFFFFFF) >> (32-bits)); +} diff --git a/src/sha1/sha1.h b/src/sha1/sha1.h new file mode 100755 index 0000000000..c0efa1c944 --- /dev/null +++ b/src/sha1/sha1.h @@ -0,0 +1,89 @@ +/* + * sha1.h + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved. + * + ***************************************************************************** + * $Id: sha1.h 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This class implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * Many of the variable names in this class, especially the single + * character names, were used because those were the names used + * in the publication. + * + * Please read the file sha1.cpp for more information. + * + */ + +#ifndef _SHA1_H_ +#define _SHA1_H_ + +class SHA1 +{ + + public: + + SHA1(); + virtual ~SHA1(); + + /* + * Re-initialize the class + */ + void Reset(); + + /* + * Returns the message digest + */ + bool Result(unsigned *message_digest_array); + + /* + * Provide input to SHA1 + */ + void Input( const unsigned char *message_array, + unsigned length); + void Input( const char *message_array, + unsigned length); + void Input(unsigned char message_element); + void Input(char message_element); + SHA1& operator<<(const char *message_array); + SHA1& operator<<(const unsigned char *message_array); + SHA1& operator<<(const char message_element); + SHA1& operator<<(const unsigned char message_element); + + private: + + /* + * Process the next 512 bits of the message + */ + void ProcessMessageBlock(); + + /* + * Pads the current message block to 512 bits + */ + void PadMessage(); + + /* + * Performs a circular left shift operation + */ + inline unsigned CircularShift(int bits, unsigned word); + + unsigned H[5]; // Message digest buffers + + unsigned Length_Low; // Message length in bits + unsigned Length_High; // Message length in bits + + unsigned char Message_Block[64]; // 512-bit message blocks + int Message_Block_Index; // Index into message block array + + bool Computed; // Is the digest computed? + bool Corrupted; // Is the message digest corruped? + +}; + +#endif diff --git a/src/sha1/shacmp.cpp b/src/sha1/shacmp.cpp new file mode 100755 index 0000000000..476ad3f0bd --- /dev/null +++ b/src/sha1/shacmp.cpp @@ -0,0 +1,169 @@ +/* + * shacmp.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved + * + ***************************************************************************** + * $Id: shacmp.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This utility will compare two files by producing a message digest + * for each file using the Secure Hashing Algorithm and comparing + * the message digests. This function will return 0 if they + * compare or 1 if they do not or if there is an error. + * Errors result in a return code higher than 1. + * + * Portability Issues: + * none. + * + */ + +#include +#include +#include "sha1.h" + +/* + * Return codes + */ +#define SHA1_COMPARE 0 +#define SHA1_NO_COMPARE 1 +#define SHA1_USAGE_ERROR 2 +#define SHA1_FILE_ERROR 3 + +/* + * Function prototype + */ +void usage(); + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * argc: [in] + * This is the count of arguments in the argv array + * argv: [in] + * This is an array of filenames for which to compute message digests + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main(int argc, char *argv[]) +{ + SHA1 sha; // SHA-1 class + FILE *fp; // File pointer for reading files + char c; // Character read from file + unsigned message_digest[2][5]; // Message digest for files + int i; // Counter + bool message_match; // Message digest match flag + int returncode; + + /* + * If we have two arguments, we will assume they are filenames. If + * we do not have to arguments, call usage() and exit. + */ + if (argc != 3) + { + usage(); + return SHA1_USAGE_ERROR; + } + + /* + * Get the message digests for each file + */ + for(i = 1; i <= 2; i++) + { + sha.Reset(); + + if (!(fp = fopen(argv[i],"rb"))) + { + fprintf(stderr, "sha: unable to open file %s\n", argv[i]); + return SHA1_FILE_ERROR; + } + + c = fgetc(fp); + while(!feof(fp)) + { + sha.Input(c); + c = fgetc(fp); + } + + fclose(fp); + + if (!sha.Result(message_digest[i-1])) + { + fprintf(stderr,"shacmp: could not compute message digest for %s\n", + argv[i]); + return SHA1_FILE_ERROR; + } + } + + /* + * Compare the message digest values + */ + message_match = true; + for(i = 0; i < 5; i++) + { + if (message_digest[0][i] != message_digest[1][i]) + { + message_match = false; + break; + } + } + + if (message_match) + { + printf("Fingerprints match:\n"); + returncode = SHA1_COMPARE; + } + else + { + printf("Fingerprints do not match:\n"); + returncode = SHA1_NO_COMPARE; + } + + printf( "\t%08X %08X %08X %08X %08X\n", + message_digest[0][0], + message_digest[0][1], + message_digest[0][2], + message_digest[0][3], + message_digest[0][4]); + printf( "\t%08X %08X %08X %08X %08X\n", + message_digest[1][0], + message_digest[1][1], + message_digest[1][2], + message_digest[1][3], + message_digest[1][4]); + + return returncode; +} + +/* + * usage + * + * Description: + * This function will display program usage information to the user. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void usage() +{ + printf("usage: shacmp \n"); + printf("\tThis program will compare the message digests (fingerprints)\n"); + printf("\tfor two files using the Secure Hashing Algorithm (SHA-1).\n"); +} diff --git a/src/sha1/shatest.cpp b/src/sha1/shatest.cpp new file mode 100755 index 0000000000..4d29ef9e24 --- /dev/null +++ b/src/sha1/shatest.cpp @@ -0,0 +1,149 @@ +/* + * shatest.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved + * + ***************************************************************************** + * $Id: shatest.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This file will exercise the SHA1 class and perform the three + * tests documented in FIPS PUB 180-1. + * + * Portability Issues: + * None. + * + */ + +#include +#include "sha1.h" + +using namespace std; + +/* + * Define patterns for testing + */ +#define TESTA "abc" +#define TESTB "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + +/* + * Function prototype + */ +void DisplayMessageDigest(unsigned *message_digest); + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main() +{ + SHA1 sha; + unsigned message_digest[5]; + + /* + * Perform test A + */ + cout << endl << "Test A: 'abc'" << endl; + + sha.Reset(); + sha << TESTA; + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D" << endl; + } + + /* + * Perform test B + */ + cout << endl << "Test B: " << TESTB << endl; + + sha.Reset(); + sha << TESTB; + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1" << endl; + } + + /* + * Perform test C + */ + cout << endl << "Test C: One million 'a' characters" << endl; + + sha.Reset(); + for(int i = 1; i <= 1000000; i++) sha.Input('a'); + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F" << endl; + } + + return 0; +} + +/* + * DisplayMessageDigest + * + * Description: + * Display Message Digest array + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void DisplayMessageDigest(unsigned *message_digest) +{ + ios::fmtflags flags; + + cout << '\t'; + + flags = cout.setf(ios::hex|ios::uppercase,ios::basefield); + cout.setf(ios::uppercase); + + for(int i = 0; i < 5 ; i++) + { + cout << message_digest[i] << ' '; + } + + cout << endl; + + cout.setf(flags); +} diff --git a/src/websocket_connection_handler.hpp b/src/websocket_connection_handler.hpp new file mode 100644 index 0000000000..33ec3b30ec --- /dev/null +++ b/src/websocket_connection_handler.hpp @@ -0,0 +1,61 @@ +#ifndef WEBSOCKET_CONNECTION_HANDLER_HPP +#define WEBSOCKET_CONNECTION_HANDLER_HPP + +#include + +#include +#include + +namespace websocketpp { + class connection_handler; + typedef boost::shared_ptr connection_handler_ptr; +} + +#include "websocket_session.hpp" + +namespace websocketpp { + +class connection_handler { +public: + // validate will be called after a websocket handshake has been received and + // before it is accepted. It provides a handler the ability to refuse a + // connection based on application specific logic (ex: restrict domains or + // negotiate subprotocols) + // + // resource is the resource requested by the connection + // headers is a map containing the HTTP headers included in the client + // handshake as key/value strings + // + // if validate returns true the connection is allowed, otherwise the + // connection is closed. + virtual bool validate(session_ptr client) = 0; + + // this will be called once the connected websocket is avaliable for + // writing messages. client may be a new websocket session or an existing + // session that was recently passed to this handler. + virtual void connect(session_ptr client) = 0; + + // this will be called when the connected websocket is no longer avaliable + // for writing messages. This occurs under the following conditions: + // - Disconnect message recieved from the remote endpoint + // - Someone (usually this object) calls the disconnect method of session + // - A disconnect acknowledgement is recieved (in case another object + // calls the disconnect method of session + // - The connection handler assigned to this client was set to another + // handler + virtual void disconnect(session_ptr client,const std::string &reason) = 0; + + // this will be called when a text message is recieved. Text will be + // encoded as UTF-8. + virtual void message(session_ptr client,const std::string &msg) = 0; + + // this will be called when a binary message is recieved. Argument is a + // vector of the raw bytes in the message body. + virtual void message(session_ptr client, + const std::vector &data) = 0; +}; + + + +} +#endif // WEBSOCKET_CONNECTION_HANDLER_HPP \ No newline at end of file diff --git a/src/websocket_frame.cpp b/src/websocket_frame.cpp new file mode 100644 index 0000000000..b71b67d39f --- /dev/null +++ b/src/websocket_frame.cpp @@ -0,0 +1,340 @@ +#include "websocket_frame.hpp" + +#include +#include + +#include + +using websocketpp::frame; + +char* frame::get_header() { + return m_header; +} + +char* frame::get_extended_header() { + return m_header+BASIC_HEADER_LENGTH; +} + +unsigned int frame::get_header_len() const { + unsigned int temp = 2; + + if (get_masked()) { + temp += 4; + } + + if (get_basic_size() == 126) { + temp += 2; + } else if (get_basic_size() == 127) { + temp += 8; + } + + return temp; +} + +char* frame::get_masking_key() { + if (m_extended_header_bytes_needed > 0) { + throw "attempted to get masking_key before reading full header"; + } + return m_masking_key; +} + +// get and set header bits +bool frame::get_fin() const { + return ((m_header[0] & BPB0_FIN) == BPB0_FIN); +} + +void frame::set_fin(bool fin) { + if (fin) { + m_header[0] |= BPB0_FIN; + } else { + m_header[0] &= (0xFF ^ BPB0_FIN); + } +} + +// get and set reserved bits +bool frame::get_rsv1() const { + return ((m_header[0] & BPB0_RSV1) == BPB0_RSV1); +} + +void frame::set_rsv1(bool b) { + if (b) { + m_header[0] |= BPB0_RSV1; + } else { + m_header[0] &= (0xFF ^ BPB0_RSV1); + } +} + +bool frame::get_rsv2() const { + return ((m_header[0] & BPB0_RSV2) == BPB0_RSV2); +} + +void frame::set_rsv2(bool b) { + if (b) { + m_header[0] |= BPB0_RSV2; + } else { + m_header[0] &= (0xFF ^ BPB0_RSV2); + } +} + +bool frame::get_rsv3() const { + return ((m_header[0] & BPB0_RSV3) == BPB0_RSV3); +} + +void frame::set_rsv3(bool b) { + if (b) { + m_header[0] |= BPB0_RSV3; + } else { + m_header[0] &= (0xFF ^ BPB0_RSV3); + } +} + +frame::opcode frame::get_opcode() const { + return frame::opcode(m_header[0] & BPB0_OPCODE); +} + +void frame::set_opcode(frame::opcode op) { + if (op > 0x0F) { + throw "invalid opcode"; + } + + if (get_basic_size() > BASIC_PAYLOAD_LIMIT && + is_control()) { + throw "control frames can't have large payloads"; + } + + m_header[0] &= (0xFF ^ BPB0_OPCODE); // clear op bits + m_header[0] |= op; // set op bits +} + +bool frame::get_masked() const { + return ((m_header[1] & BPB1_MASK) == BPB1_MASK); +} + +void frame::set_masked(bool masked) { + if (masked) { + m_header[1] |= BPB1_MASK; + generate_masking_key(); + } else { + m_header[1] &= (0xFF ^ BPB1_MASK); + clear_masking_key(); + } +} + +uint8_t frame::get_basic_size() const { + return m_header[1] & BPB1_PAYLOAD; +} + +size_t frame::get_payload_size() const { + if (m_extended_header_bytes_needed > 0) { + // problem + throw "attempted to get payload size before reading full header"; + } + + return m_payload.size(); +} + +std::vector &frame::get_payload() { + return m_payload; +} + +void frame::set_payload(const std::vector source) { + set_payload_helper(source.size()); + + std::copy(source.begin(),source.end(),m_payload.begin()); +} +void frame::set_payload(const std::string source) { + set_payload_helper(source.size()); + + std::copy(source.begin(),source.end(),m_payload.begin()); +} + +bool frame::is_control() const { + return (get_opcode() > MAX_FRAME_OPCODE); +} + +void frame::set_payload_helper(size_t s) { + if (s > max_payload_size) { + throw "requested payload is over implimentation defined limit"; + } + + // limits imposed by the websocket spec + if (s > BASIC_PAYLOAD_LIMIT && + get_opcode() > MAX_FRAME_OPCODE) { + throw "control frames can't have large payloads"; + } + + if (s <= BASIC_PAYLOAD_LIMIT) { + m_header[1] = s; + } else if (s <= PAYLOAD_16BIT_LIMIT) { + m_header[1] = BASIC_PAYLOAD_16BIT_CODE; + + // this reinterprets the second pair of bytes in m_header as a + // 16 bit int and writes the payload size there as an integer + // in network byte order + *reinterpret_cast(&m_header[BASIC_HEADER_LENGTH]) = htons(s); + } else if (s <= PAYLOAD_64BIT_LIMIT) { + m_header[1] = BASIC_PAYLOAD_64BIT_CODE; + *reinterpret_cast(&m_header[BASIC_HEADER_LENGTH]) = htonll(s); + } else { + throw "payload size limit is 63 bits"; + } + + m_payload.resize(s); +} + +void frame::print_frame() const { + /*unsigned int len = get_header_len(); + + std::cout << "frame: "; + // print header + for (unsigned int i = 0; i < len; i++) { + std::cout << std::hex << (unsigned short)m_header[i] << " "; + } + // print message + for (auto &i: m_payload) { + std::cout << i; + } + + std::cout << std::endl;*/ +} + +unsigned int frame::process_basic_header() { + m_extended_header_bytes_needed = 0; + m_payload.empty(); + + m_extended_header_bytes_needed = get_header_len() - BASIC_HEADER_LENGTH; + + return m_extended_header_bytes_needed; +} + +void frame::process_extended_header() { + m_extended_header_bytes_needed = 0; + + uint8_t s = get_basic_size(); + uint64_t payload_size; + int mask_index = BASIC_HEADER_LENGTH; + + if (s <= BASIC_PAYLOAD_LIMIT) { + payload_size = s; + } else if (s == BASIC_PAYLOAD_16BIT_CODE) { + // reinterpret the second two bytes as a 16 bit integer in network + // byte order. Convert to host byte order and store locally. + payload_size = ntohs(*( + reinterpret_cast(&m_header[BASIC_HEADER_LENGTH]) + )); + + mask_index += 2; + } else if (s == BASIC_PAYLOAD_64BIT_CODE) { + // reinterpret the second eight bytes as a 16 bit integer in + // network byte order. Convert to host byte order and store. + payload_size = ntohll(*( + reinterpret_cast(&m_header[BASIC_HEADER_LENGTH]) + )); + + mask_index += 8; + } else { + // shouldn't be here + throw "invalid get_basic_size in process_extended_header"; + } + + if (payload_size < s) { + throw "payload size error"; + } + + if (get_masked() == 0) { + clear_masking_key(); + } else { + // TODO: use this copy line (needs testing) + // std::copy(m_header[mask_index],m_header[mask_index+4],m_masking_key); + m_masking_key[0] = m_header[mask_index+0]; + m_masking_key[1] = m_header[mask_index+1]; + m_masking_key[2] = m_header[mask_index+2]; + m_masking_key[3] = m_header[mask_index+3]; + } + + if (payload_size > max_payload_size) { + throw "got frame with payload greater than maximum frame buffer size."; + } + m_payload.resize(payload_size); +} + +void frame::process_payload() { + // unmask payload one byte at a time + for (uint64_t i = 0; i < m_payload.size(); i++) { + m_payload[i] = (m_payload[i] ^ m_masking_key[i%4]); + } +} + +void frame::process_payload2() { + // unmask payload one byte at a time + + //uint64_t key = (*((uint32_t*)m_masking_key;)) << 32; + //key += *((uint32_t*)m_masking_key); + + // might need to switch byte order + uint32_t key = *((uint32_t*)m_masking_key); + + // 4 + + uint64_t i = 0; + uint64_t s = (m_payload.size() / 4); + + std::cout << "s: " << s << std::endl; + + // chunks of 4 + for (i = 0; i < s; i+=4) { + ((uint32_t*)(&m_payload[0]))[i] = (((uint32_t*)(&m_payload[0]))[i] ^ key); + } + + // finish the last few + for (i = s; i < m_payload.size(); i++) { + m_payload[i] = (m_payload[i] ^ m_masking_key[i%4]); + } +} + +bool frame::validate_basic_header() const { + // check for control frame size + if (get_basic_size() > BASIC_PAYLOAD_LIMIT && is_control()) { + return false; + } + + // check for reserved opcodes + if (get_rsv1() || get_rsv2() || get_rsv3()) { + return false; + } + + // check for reserved opcodes + opcode op = get_opcode(); + if (op > 0x02 && op < 0x08) { + return false; + } + if (op > 0x0A) { + return false; + } + + // check for fragmented control message + if (is_control() && !get_fin()) { + return false; + } + + return true; +} + +void frame::generate_masking_key() { + throw "masking key generation not implimented"; + /* TODO: test and tune + + boost::random::random_device rng; + boost::random::uniform_int_distribution<> mask(0,UINT32_MAX); + + *(reinterpret_cast(m_masking_key)) = mask(rng); + + */ +} + +void frame::clear_masking_key() { + m_masking_key[0] = 0; + m_masking_key[1] = 0; + m_masking_key[2] = 0; + m_masking_key[3] = 0; +} \ No newline at end of file diff --git a/src/websocket_frame.hpp b/src/websocket_frame.hpp new file mode 100644 index 0000000000..a010117615 --- /dev/null +++ b/src/websocket_frame.hpp @@ -0,0 +1,113 @@ +#ifndef WEBSOCKET_FRAME_HPP +#define WEBSOCKET_FRAME_HPP + +#include "network_utilities.hpp" + +#include +#include +#include + +namespace websocketpp { + +class frame { +public: + enum opcode_s { + CONTINUATION_FRAME = 0x00, + TEXT_FRAME = 0x01, + BINARY_FRAME = 0x02, + CONNECTION_CLOSE = 0x08, + PING = 0x09, + PONG = 0x0A + }; + + typedef enum opcode_s opcode; + + static const uint8_t MAX_FRAME_OPCODE = 0x07; + + // basic payload byte flags + static const uint8_t BPB0_OPCODE = 0x0F; + static const uint8_t BPB0_RSV3 = 0x10; + static const uint8_t BPB0_RSV2 = 0x20; + static const uint8_t BPB0_RSV1 = 0x40; + static const uint8_t BPB0_FIN = 0x80; + static const uint8_t BPB1_PAYLOAD = 0x7F; + static const uint8_t BPB1_MASK = 0x80; + + static const uint8_t BASIC_PAYLOAD_LIMIT = 0x7D; // 125 + static const uint8_t BASIC_PAYLOAD_16BIT_CODE = 0x7E; // 126 + static const uint16_t PAYLOAD_16BIT_LIMIT = 0xFFFF; // 2^16, 65535 + static const uint8_t BASIC_PAYLOAD_64BIT_CODE = 0x7F; // 127 + static const uint64_t PAYLOAD_64BIT_LIMIT = 0x7FFFFFFFFFFFFFFF; // 2^63 + + static const unsigned int BASIC_HEADER_LENGTH = 2; + static const unsigned int MAX_HEADER_LENGTH = 14; + static const uint8_t extended_header_length = 12; + static const uint64_t max_payload_size = 100000000; // 100MB + + // create an empty frame for writing into + frame() { + // not sure if these are necessary with c++ but putting in just in case + memset(m_header,0,MAX_HEADER_LENGTH); + } + + // get pointers to underlying buffers + char* get_header(); + char* get_extended_header(); + unsigned int get_header_len() const; + + char* get_masking_key(); + + // get and set header bits + bool get_fin() const; + void set_fin(bool fin); + + bool get_rsv1() const; + void set_rsv1(bool b); + + bool get_rsv2() const; + void set_rsv2(bool b); + + bool get_rsv3() const; + void set_rsv3(bool b); + + opcode get_opcode() const; + void set_opcode(opcode op); + + bool get_masked() const; + void set_masked(bool masked); + + uint8_t get_basic_size() const; + size_t get_payload_size() const; + + std::vector &get_payload(); + + void set_payload(const std::vector source); + void set_payload(const std::string source); + void set_payload_helper(size_t s); + + bool is_control() const; + + void print_frame() const; + + // reads basic header, sets and returns m_header_bits_needed + unsigned int process_basic_header(); + void process_extended_header(); + void process_payload(); + void process_payload2(); // experiment with more efficient masking code. + + bool validate_basic_header() const; + + void generate_masking_key(); + void clear_masking_key(); + +private: + char m_header[MAX_HEADER_LENGTH]; + std::vector m_payload; + + char m_masking_key[4]; + unsigned int m_extended_header_bytes_needed; +}; + +} + +#endif // WEBSOCKET_FRAME_HPP \ No newline at end of file diff --git a/src/websocket_server.cpp b/src/websocket_server.cpp new file mode 100644 index 0000000000..5f6665ad8b --- /dev/null +++ b/src/websocket_server.cpp @@ -0,0 +1,44 @@ +#include "websocket_server.hpp" + +#include + +#include + +using websocketpp::server; + +server::server(boost::asio::io_service& io_service, + const tcp::endpoint& endpoint, + const std::string& host, + connection_handler_ptr defc) + : m_host(host), + m_io_service(io_service), + m_acceptor(io_service, endpoint), + m_def_con_handler(defc) { + this->start_accept(); +} + +void server::start_accept() { + session_ptr new_ws(new session(m_io_service,m_host,m_def_con_handler)); + + m_acceptor.async_accept( + new_ws->socket(), + boost::bind( + &server::handle_accept, + this, + new_ws, + boost::asio::placeholders::error + ) + ); +} + +void server::handle_accept(session_ptr session, + const boost::system::error_code& error) { + + if (!error) { + session->start(); + } else { + std::cout << "Error" << std::endl; + } + + this->start_accept(); +} \ No newline at end of file diff --git a/src/websocket_server.hpp b/src/websocket_server.hpp new file mode 100644 index 0000000000..6818ee1570 --- /dev/null +++ b/src/websocket_server.hpp @@ -0,0 +1,40 @@ +#ifndef WEBSOCKET_SERVER_HPP +#define WEBSOCKET_SERVER_HPP + +#include "websocket_session.hpp" + +#include +#include + +using boost::asio::ip::tcp; + +namespace websocketpp { + +class server { + public: + server(boost::asio::io_service& io_service, + const tcp::endpoint& endpoint, + const std::string& host, + connection_handler_ptr defc); + + // creates a new session object and connects the next websocket + // connection to it. + void start_accept(); + + // if no errors starts the session's read loop and returns to the + // start_accept phase. + void handle_accept(session_ptr session, + const boost::system::error_code& error); + + private: + std::string m_host; + boost::asio::io_service& m_io_service; + tcp::acceptor m_acceptor; + connection_handler_ptr m_def_con_handler; +}; + +typedef boost::shared_ptr server_ptr; + +} + +#endif // WEBSOCKET_SERVER_HPP \ No newline at end of file diff --git a/src/websocket_session.cpp b/src/websocket_session.cpp new file mode 100644 index 0000000000..8d68e65898 --- /dev/null +++ b/src/websocket_session.cpp @@ -0,0 +1,666 @@ +#include "websocket_session.hpp" + +#include "websocket_frame.hpp" + +#include +#include +#include + +#include +#include +#include + +using websocketpp::session; + +void session::start() { + //std::cout << "[Connection " << this << "] WebSocket Connection request from " << m_socket.remote_endpoint() << std::endl; + + // async read to handle_read_handshake + boost::asio::async_read_until( + m_socket, + m_buf, + "\r\n\r\n", + boost::bind( + &session::handle_read_handshake, + shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred + ) + ); +} + +void session::set_handler(connection_handler_ptr new_con) { + if (m_local_interface) { + m_local_interface->disconnect(shared_from_this(),"Setting new connection handler"); + } + m_local_interface = new_con; + m_local_interface->connect(shared_from_this()); +} + + +std::string session::get_header(const std::string& key) const { + std::map::const_iterator h = m_headers.find(key); + + if (h == m_headers.end()) { + return std::string(); + } else { + return h->second; + } +} + +std::string session::get_request() const { + return m_request; +} + +void session::set_http_error(int code, std::string msg) { + m_http_error_code = code; + m_http_error_string = (msg != "" ? msg : lookup_http_error_string(code)); +} + +std::string session::lookup_http_error_string(int code) { + switch (code) { + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + 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 Implimented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + default: + return "Unknown"; + } +} + +void session::send(const std::string &msg) { + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::TEXT_FRAME); + m_write_frame.set_payload(msg); + + write_frame(); +} + +// send binary frame +void session::send(const std::vector &data) { + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::BINARY_FRAME); + m_write_frame.set_payload(data); + + write_frame(); +} + +// send close frame +void session::disconnect(const std::string &reason) { + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::CONNECTION_CLOSE); + m_write_frame.set_payload(reason); + + write_frame(); + + if (m_local_interface) { + m_local_interface->disconnect(shared_from_this(),reason); + } +} + +void session::ping(const std::string &msg) { + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::PING); + m_write_frame.set_payload(msg); + + write_frame(); +} + +void session::pong(const std::string &msg) { + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::PONG); + m_write_frame.set_payload(msg); + + write_frame(); +} + +void session::handle_read_handshake(const boost::system::error_code& e, + std::size_t bytes_transferred) { + // read handshake and set local state (or pass to write_handshake) + std::ostringstream line; + line << &m_buf; + m_handshake += line.str(); + + //std::cout << "=== Raw Message ===" << std::endl; + //std::cout << m_handshake << std::endl; + //std::cout << "=== Raw Message end ===" << std::endl; + + std::vector tokens; + std::string::size_type start = 0; + std::string::size_type end; + + // Get request and parse headers + end = m_handshake.find("\r\n",start); + + while(end != std::string::npos) { + tokens.push_back(m_handshake.substr(start, end - start)); + + start = end + 2; + + end = m_handshake.find("\r\n",start); + } + + for (size_t i = 0; i < tokens.size(); i++) { + if (i == 0) { + m_request = tokens[i]; + } + + end = tokens[i].find(": ",0); + + if (end != std::string::npos) { + m_headers[tokens[i].substr(0,end)] = tokens[i].substr(end+2); + } + } + + // error checking + + // check the method + if (m_request.substr(0,4) != "GET ") { + // invalid method + std::cout << "Websocket handshake has invalid method: " << m_request.substr(0,4) << ", killing connection." << std::endl; + // TODO: exception + this->set_http_error(400); + this->write_http_error(); + return; + } + + // check the HTTP version + // TODO: allow versions greater than 1.1 + end = m_request.find(" HTTP/1.1",4); + if (end == std::string::npos) { + std::cout << "Websocket handshake has invalid HTTP version, killing connection." << std::endl; + this->set_http_error(400); // or error 505 HTTP Version Not Supported + this->write_http_error(); + return; + } + + m_request = m_request.substr(4,end-4); + + // TODO: use exceptions or a helper function or something better here + + // verify the presence of required headers + if (get_header("Host") != m_host) { + std::cerr << "Invalid or missing Host header." << std::endl; + this->set_http_error(400); + } + if (!boost::iequals(get_header("Upgrade"),"websocket")) { + std::cerr << "Invalid or missing Upgrade header." << std::endl; + this->set_http_error(400); + } + if (get_header("Connection").find("Upgrade") == std::string::npos) { + // TODO: case insensitive? + std::cerr << "Invalid or missing Connection header." << std::endl; + this->set_http_error(400); + } + if (get_header("Sec-WebSocket-Key") == "") { + std::cerr << "Invalid or missing Sec-Websocket-Key header." << std::endl; + this->set_http_error(400); + } + + if (get_header("Sec-WebSocket-Version") != "8" && + get_header("Sec-WebSocket-Version") != "7") { + std::cerr << "Invalid or missing Sec-Websocket-Version header." << std::endl; + this->set_http_error(400); + } + + if (m_http_error_code != 0) { + this->write_http_error(); + return; + } + + // optional headers (delegated to the local interface) + if (m_local_interface && !m_local_interface->validate(shared_from_this())) { + std::cerr << "Local interface rejected the connection." << std::endl; + if (m_http_error_code == 0) { + this->set_http_error(400); + } + } + + if (m_http_error_code != 0) { + this->write_http_error(); + return; + } + + this->write_handshake(); +} + +void session::write_handshake() { + std::string server_handshake = ""; + std::string server_key = m_headers["Sec-WebSocket-Key"]; + server_key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + SHA1 sha; + uint32_t message_digest[5]; + + sha.Reset(); + sha << server_key.c_str(); + + if (!sha.Result(message_digest)) { + std::cerr << "Error computing sha1 hash, killing connection." << std::endl; + + return; + } + + // convert sha1 hash bytes to network byte order because this sha1 + // library works on ints rather than bytes + for (int i = 0; i < 5; i++) { + message_digest[i] = htonl(message_digest[i]); + } + + server_key = base64_encode( + reinterpret_cast(message_digest),20); + + server_handshake += "HTTP/1.1 101 Switching Protocols\r\n"; + server_handshake += "Upgrade: websocket\r\n"; + server_handshake += "Connection: Upgrade\r\n"; + server_handshake += "Sec-WebSocket-Accept: "+server_key+"\r\n\r\n"; + + // TODO: handler requested headers + + //std::cout << server_handshake << std::endl; + + // start async write to handle_write_handshake + boost::asio::async_write( + m_socket, + boost::asio::buffer(server_handshake), + boost::bind( + &session::handle_write_handshake, + shared_from_this(), + boost::asio::placeholders::error + ) + ); +} + +void session::handle_write_handshake(const boost::system::error_code& error) { + if (error) { + handle_error("Error writing handshake",error); + return; + } + + //std::cout << "WebSocket Version 8 connection opened." << std::endl; + + m_status = OPEN; + + if (m_local_interface) { + m_local_interface->connect(shared_from_this()); + } + + reset_message(); + this->read_frame(); +} + +void session::write_http_error() { + std::stringstream server_handshake; + + server_handshake << "HTTP/1.1 " << m_http_error_code << " " + << m_http_error_string << "\r\n"; + + // additional headers? + + server_handshake << "\r\n"; + + // start async write to handle_write_handshake + boost::asio::async_write( + m_socket, + boost::asio::buffer(server_handshake.str()), + boost::bind( + &session::handle_write_http_error, + shared_from_this(), + boost::asio::placeholders::error + ) + ); +} + +void session::handle_write_http_error(const boost::system::error_code& error) { + if (error) { + handle_error("Error writing http response",error); + return; + } +} + +void session::read_frame() { + boost::asio::async_read( + m_socket, + boost::asio::buffer(m_read_frame.get_header(), + frame::BASIC_HEADER_LENGTH), + boost::bind( + &session::handle_frame_header, + shared_from_this(), + boost::asio::placeholders::error + ) + ); +} + + + +void session::handle_frame_header(const boost::system::error_code& error) { + if (error) { + handle_error("Error reading basic frame header",error); + return; + } + + uint16_t extended_header_bytes = m_read_frame.process_basic_header(); + + if (!m_read_frame.validate_basic_header()) { + handle_error("Basic header validation failed",boost::system::error_code()); + return; + } + + if (extended_header_bytes == 0) { + read_payload(); + } else { + boost::asio::async_read( + m_socket, + boost::asio::buffer(m_read_frame.get_extended_header(), + extended_header_bytes), + boost::bind( + &session::handle_extended_frame_header, + shared_from_this(), + boost::asio::placeholders::error + ) + ); + } +} + +void session::handle_extended_frame_header( + const boost::system::error_code& error) { + if (error) { + handle_error("Error reading extended frame header",error); + return; + } + + // this sets up the buffer we are about to read into. + m_read_frame.process_extended_header(); + + this->read_payload(); +} + +void session::read_payload() { + /*char * foo = m_read_frame.get_header(); + + std::cout << std::hex << ((uint16_t*)foo)[0] << std::endl; + + std::cout << "opcode: " << m_read_frame.get_opcode() << std::endl; + std::cout << "fin: " << m_read_frame.get_fin() << std::endl; + std::cout << "mask: " << m_read_frame.get_masked() << std::endl; + std::cout << "size: " << (uint16_t)m_read_frame.get_basic_size() << std::endl; + std::cout << "payload_size: " << m_read_frame.get_payload_size() << std::endl;*/ + + boost::asio::async_read( + m_socket, + boost::asio::buffer(m_read_frame.get_payload()), + boost::bind( + &session::handle_read_payload, + shared_from_this(), + boost::asio::placeholders::error + ) + ); +} + +void session::handle_read_payload (const boost::system::error_code& error) { + if (error) { + handle_error("Error reading payload data frame header",error); + return; + } + + m_read_frame.process_payload(); + + switch (m_read_frame.get_opcode()) { + case frame::CONTINUATION_FRAME: + process_continuation(); + break; + case frame::TEXT_FRAME: + process_text(); + break; + case frame::BINARY_FRAME: + process_binary(); + break; + case frame::CONNECTION_CLOSE: + process_close(); + break; + case frame::PING: + process_ping(); + break; + case frame::PONG: + process_pong(); + break; + default: + // TODO: unknown frame type + // ignore or close? + break; + } + + // check if there was an error processing this frame and fail the connection + if (m_error) { + return; + } + + this->read_frame(); +} + +void session::handle_write_frame (const boost::system::error_code& error) { + if (error) { + handle_error("Error writing frame data",error); + } + + //std::cout << "Successfully wrote frame." << std::endl; +} + +void session::process_ping() { + std::cout << "Got ping" << std::endl; + + // send pong + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::PONG); + m_write_frame.set_payload(m_read_frame.get_payload()); + + write_frame(); +} + +void session::process_pong() { + std::cout << "Got pong" << std::endl; +} + +void session::process_text() { + // text is binary. + process_binary(); +} + +void session::process_binary() { + if (m_fragmented) { + handle_error("Got a new message before the previous was finished.", + boost::system::error_code()); + return; + } + + m_current_opcode = m_read_frame.get_opcode(); + + if (m_read_frame.get_fin()) { + deliver_message(); + reset_message(); + } else { + m_fragmented = true; + extract_payload(); + } +} + +void session::process_continuation() { + if (!m_fragmented) { + handle_error("Got a continuation frame without an outstanding message.", + boost::system::error_code()); + return; + } + + extract_payload(); + + // check if we are done + if (m_read_frame.get_fin()) { + deliver_message(); + reset_message(); + } +} + +void session::process_close() { + if (m_status == OPEN) { + // send response and set to closed + std::string msg(m_read_frame.get_payload().begin(), + m_read_frame.get_payload().end()); + + std::cout << "Got connection close message, acking and closing the connection. Reason was: " << msg << std::endl; + + m_status = CLOSED; + + // send acknowledgement + m_write_frame.set_fin(true); + m_write_frame.set_opcode(frame::CONNECTION_CLOSE); + m_write_frame.set_payload(""); + + write_frame(); + + // let our local interface know that the remote client has + // disconnected + if (m_local_interface) { + m_local_interface->disconnect(shared_from_this(),msg); + } + } else if (m_status == CLOSING) { + // this is an ack of my close message + // close cleanly + std::cout << "Got ack for my close message, closing the connection" << std::endl; + m_status = CLOSED; + + // let our local interface know that the remote client has + // disconnected and the reason (if any) + if (m_local_interface) { + m_local_interface->disconnect(shared_from_this(),""); + } + } else { + // ignore + } +} + +void session::deliver_message() { + if (!m_local_interface) { + return; + } + + if (m_current_opcode == frame::BINARY_FRAME) { + if (m_fragmented) { + m_local_interface->message(shared_from_this(),m_current_message); + } else { + m_local_interface->message(shared_from_this(), + m_read_frame.get_payload()); + } + } else if (m_current_opcode == frame::TEXT_FRAME) { + std::string msg; + + if (m_fragmented) { + msg.append(m_current_message.begin(),m_current_message.end()); + } else { + msg.append( + m_read_frame.get_payload().begin(), + m_read_frame.get_payload().end() + ); + } + + m_local_interface->message(shared_from_this(),msg); + } else { + // fail + } + +} + +void session::extract_payload() { + std::vector &msg = m_read_frame.get_payload(); + m_current_message.resize(m_current_message.size()+msg.size()); + std::copy(msg.begin(),msg.end(),m_current_message.end()-msg.size()); +} + +void session::write_frame() { + // print debug info + m_write_frame.print_frame(); + + std::vector data; + + data.push_back( + boost::asio::buffer( + m_write_frame.get_header(), + m_write_frame.get_header_len() + ) + ); + data.push_back( + boost::asio::buffer(m_write_frame.get_payload()) + ); + + boost::asio::async_write( + m_socket, + data, + boost::bind( + &session::handle_write_frame, + shared_from_this(), + boost::asio::placeholders::error + ) + ); +} + +void session::reset_message() { + m_error = false; + m_fragmented = false; + m_current_message.clear(); +} + +void session::handle_error(std::string msg, + const boost::system::error_code& error) { + std::stringstream e; + + e << msg << " (" << error << ")"; + + std::cerr << e.str() << std::endl; + + if (m_local_interface) { + m_local_interface->disconnect(shared_from_this(),e.str()); + } + + m_error = true; +} \ No newline at end of file diff --git a/src/websocket_session.hpp b/src/websocket_session.hpp new file mode 100644 index 0000000000..479337ff93 --- /dev/null +++ b/src/websocket_session.hpp @@ -0,0 +1,209 @@ +#ifndef WEBSOCKET_SESSION_HPP +#define WEBSOCKET_SESSION_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +namespace websocketpp { + class session; + typedef boost::shared_ptr session_ptr; +} + +#include "websocket_frame.hpp" +#include "websocket_connection_handler.hpp" + +#include "base64/base64.h" +#include "sha1/sha1.h" + +using boost::asio::ip::tcp; + +namespace websocketpp { + +class session : public boost::enable_shared_from_this { +public: + enum ws_status { + CONNECTING, + OPEN, + CLOSING, + CLOSED + }; + + typedef enum ws_status status_code; + + session (boost::asio::io_service& io_service, + const std::string& host, + connection_handler_ptr defc) + : m_host(host), + m_socket(io_service), + m_status(CONNECTING), + m_http_error_code(0), + m_local_interface(defc) {} + + tcp::socket& socket() { + return m_socket; + } + + // This function is called to begin the session loop. This method and all + // that come after it are called as a result of an async event completing. + // if any method in this chain returns before adding a new async event the + // session will end. + void start(); + + // sets the internal connection handler of this connection to new_con. + // This is useful if you want to switch handler objects during a connection + // Example: a generic lobby handler could validate the handshake negotiate a + // sub protocol to talk to and then pass the connection off to a handler for + // that sub protocol. + void set_handler(connection_handler_ptr new_con); + + + // Public handshake interface + + // By default a failed handshake validation will return an HTTP 400 Bad + // Request error. If your application wants to reject the connection for + // another reason it can be set here. Example: 404 if the resource request + // is not recognized or 403 forbidden if the origin does not match. If only + // a code is supplied a msg will be generated automatically. + void set_http_error(int code,std::string msg = ""); + + // gets the value of a header or the empty string if not present. + std::string get_header(const std::string &key) const; + // adds an arbitrary header to the server handshake HTTP response. + void add_header(const std::string &key,const std::string &value); + + std::string get_request() const; + + // sets the subprotocol being used. This will result in the appropriate + // Sec-WebSocket-Protocol header being sent back to the client. The value + // here must have been present in the client's opening handshake. + void set_subprotocol(const std::string &protocol); + + + //int get_version(); + + //void add_extension(); + + // Public session interface + + // send basic frame types + void send(const std::string &msg); // text + void send(const std::vector &data); // binary + void ping(const std::string &msg); + void pong(const std::string &msg); + + void disconnect(const std::string &reason); +private: + // handle_read_handshake reads the HTTP headers of the initial websocket + // handshake, parses out the request and headers, and does error checking + // TODO: Generalize a lot of the hard coded things in this method. + void handle_read_handshake(const boost::system::error_code& e, + std::size_t bytes_transferred); + + // write_handshake calculates the server portion of the handshake and + // sends it back. + // TODO: Generalize this to include things like protocols, cookies, etc + void write_handshake(); + // handle_write_handshake checks for errors writing the server handshake, + // officially declares a connection open, notifies the local interface, + // and starts the frame reading loop. + void handle_write_handshake(const boost::system::error_code& error); + + // construct and write an HTTP error in the case the handshake goes poorly + void write_http_error(); + void handle_write_http_error(const boost::system::error_code& error); + + // start async read for a websocket frame (2 bytes) to handle_frame_header + void read_frame(); + + // reads frame header and devices if it needs to read more header or go + // straight to the payload. + void handle_frame_header(const boost::system::error_code& error); + + // process extra headers and start payload read + void handle_extended_frame_header(const boost::system::error_code& error); + + // initiate payload read + void read_payload(); + + // now the frame object should be complete. Process and send it on then + // reset for new frame + void handle_read_payload (const boost::system::error_code& error); + + // checks for errors writing frames + void handle_write_frame (const boost::system::error_code& error); + + // helper functions for processing each opcode + void process_ping(); + void process_pong(); + void process_text(); + void process_binary(); + void process_continuation(); + void process_close(); + + // deliver message if we have a local interface attached + void deliver_message(); + + // copies the current read frame payload into the session so that the read + // frame can be cleared for the next read. This is done when fragmented + // messages are recieved. + void extract_payload(); + + // write m_write_frame out to the socket. + void write_frame(); + + // reset session for a new message + void reset_message(); + + // prints a diagnostic message and disconnects the local interface + void handle_error(std::string msg,const boost::system::error_code& error); + + std::string lookup_http_error_string(int code); +private: + std::string m_host; + tcp::socket m_socket; + status_code m_status; + int m_http_error_code; + std::string m_http_error_string; + + boost::asio::streambuf m_buf; + std::string m_handshake; + + std::string m_request; + std::map m_headers; + + frame m_read_frame; + frame m_write_frame; + std::vector m_current_message; + + bool m_error; + bool m_fragmented; + frame::opcode m_current_opcode; + + connection_handler_ptr m_local_interface; +}; + +} + +#endif // WEBSOCKET_SESSION_HPP + + + +// better debug printing system +// set acceptible origin and host headers +// case sensitive header values? e.g. websocket + + +// double check bugs in autobahn (sending wrong localhost:9000 header) not +// checking masking in the 9.x tests diff --git a/src/websocketpp.hpp b/src/websocketpp.hpp new file mode 100644 index 0000000000..d798ef11db --- /dev/null +++ b/src/websocketpp.hpp @@ -0,0 +1,6 @@ +#ifndef WEBSOCKETPP_HPP +#define WEBSOCKETPP_HPP + +#include + +#endif // WEBSOCKETPP_HPP diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000000..809a2d1860 --- /dev/null +++ b/todo.txt @@ -0,0 +1,11 @@ +// TODO: impliment stuff from here: +// http://stackoverflow.com/questions/809902/64-bit-ntohl-in-c + +boost unit test suite? + +figure out exception handling setup + + +nagle? + +extensions \ No newline at end of file