diff options
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | erlang.mk | 678 | ||||
-rw-r--r-- | include/cow_inline.hrl | 2 | ||||
-rw-r--r-- | src/cow_cookie.erl | 2 | ||||
-rw-r--r-- | src/cow_date.erl | 6 | ||||
-rw-r--r-- | src/cow_http.erl | 6 | ||||
-rw-r--r-- | src/cow_http_hd.erl | 66 | ||||
-rw-r--r-- | src/cow_http_te.erl | 7 | ||||
-rw-r--r-- | src/cow_mimetypes.erl | 2 | ||||
-rw-r--r-- | src/cow_multipart.erl | 19 | ||||
-rw-r--r-- | src/cow_qs.erl | 10 | ||||
-rw-r--r-- | src/cow_spdy.erl | 2 | ||||
-rw-r--r-- | src/cow_ws.erl | 473 | ||||
-rw-r--r-- | src/cowlib.app.src | 4 | ||||
-rw-r--r-- | test/eunit_SUITE.erl | 31 |
16 files changed, 1014 insertions, 310 deletions
@@ -1,4 +1,4 @@ -Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -1,8 +1,13 @@ # See LICENSE for licensing information. PROJECT = cowlib +#ERLC_OPTS += +bin_opt_info +TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}' +'{parse_transform, horse_autoexport}' PLT_APPS = crypto +TEST_DEPS = triq +dep_triq = git https://github.com/krestenkrab/triq master + include erlang.mk .PHONY: gen perfs @@ -32,12 +37,15 @@ gen: # Performance testing. +ifeq ($(MAKECMDGOALS),perfs) +.NOTPARALLEL: +endif + deps/horse: git clone -n -- https://github.com/extend/horse $(DEPS_DIR)/horse cd $(DEPS_DIR)/horse ; git checkout -q master $(MAKE) -C $(DEPS_DIR)/horse -perfs: ERLC_OPTS += -DPERF=1 +'{parse_transform, horse_autoexport}' -DEXTRA=1 -perfs: clean deps deps/horse app +perfs: test-build $(gen_verbose) erl -noshell -pa ebin deps/horse/ebin \ - -eval 'horse:app_perf($(PROJECT)), init:stop().' + -eval 'horse:app_perf($(PROJECT)), erlang:halt().' @@ -1,4 +1,4 @@ -# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -28,12 +28,30 @@ V ?= 0 gen_verbose_0 = @echo " GEN " $@; gen_verbose = $(gen_verbose_$(V)) +# "erl" command. + +ERL = erl +A0 -noinput -boot start_clean + # Core targets. -all:: deps app rel +ifneq ($(words $(MAKECMDGOALS)),1) +.NOTPARALLEL: +endif -clean:: +all:: deps + @$(MAKE) --no-print-directory app + @$(MAKE) --no-print-directory rel + +# Noop to avoid a Make warning when there's nothing to do. +rel:: + @echo -n + +clean:: clean-crashdump + +clean-crashdump: +ifneq ($(wildcard erl_crash.dump),) $(gen_verbose) rm -f erl_crash.dump +endif distclean:: clean @@ -42,7 +60,7 @@ help:: "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ "Copyright (c) 2013-2014 Loïc Hoguin <[email protected]>" \ "" \ - "Usage: [V=1] make [target]" \ + "Usage: [V=1] make [-jNUM] [target]" \ "" \ "Core targets:" \ " all Run deps, app and rel targets in that order" \ @@ -58,7 +76,8 @@ help:: "The target clean only removes files that are commonly removed." \ "Dependencies and releases are left untouched." \ "" \ - "Setting V=1 when calling make enables verbose mode." + "Setting V=1 when calling make enables verbose mode." \ + "Parallel execution is supported through the -j Make flag." # Core functions. @@ -68,7 +87,7 @@ define core_http_get endef else define core_http_get - erl -noshell -eval 'ssl:start(), inets:start(), case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of {ok, {{_, 200, _}, _, Body}} -> case file:write_file("$(1)", Body) of ok -> ok; {error, R1} -> halt(R1) end; {error, R2} -> halt(R2) end, halt(0).' + $(ERL) -eval 'ssl:start(), inets:start(), case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of {ok, {{_, 200, _}, _, Body}} -> case file:write_file("$(1)", Body) of ok -> ok; {error, R1} -> halt(R1) end; {error, R2} -> halt(R2) end, halt(0).' endef endif @@ -84,13 +103,16 @@ erlang-mk: cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk rm -rf $(ERLANG_MK_BUILD_DIR) -# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-deps distclean-pkg pkg-list pkg-search # Configuration. +AUTOPATCH ?= edown gen_leader gproc +export AUTOPATCH + DEPS_DIR ?= $(CURDIR)/deps export DEPS_DIR @@ -128,6 +150,41 @@ distclean:: distclean-deps distclean-pkg # Deps related targets. +define dep_autopatch + $(ERL) -eval " \ +DepDir = \"$(DEPS_DIR)/$(1)/\", \ +fun() -> \ + {ok, Conf} = file:consult(DepDir ++ \"rebar.config\"), \ + File = case lists:keyfind(deps, 1, Conf) of false -> []; {_, Deps} -> \ + [begin {Method, Repo, Commit} = case Repos of \ + {git, R} -> {git, R, master}; \ + {M, R, {branch, C}} -> {M, R, C}; \ + {M, R, {tag, C}} -> {M, R, C}; \ + {M, R, C} -> {M, R, C} \ + end, \ + io_lib:format(\"DEPS += ~s\ndep_~s = ~s ~s ~s~n\", [Name, Name, Method, Repo, Commit]) \ + end || {Name, _, Repos} <- Deps] \ + end, \ + ok = file:write_file(\"$(DEPS_DIR)/$(1)/Makefile\", [\"ERLC_OPTS = +debug_info\n\n\", File, \"\ninclude erlang.mk\"]) \ +end(), \ +AppSrcOut = \"$(DEPS_DIR)/$(1)/src/$(1).app.src\", \ +AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> \"$(DEPS_DIR)/$(1)/ebin/$(1).app\"; true -> AppSrcOut end, \ +fun() -> \ + {ok, [{application, $(1), L}]} = file:consult(AppSrcIn), \ + L2 = case lists:keyfind(modules, 1, L) of {_, _} -> L; false -> [{modules, []}|L] end, \ + L3 = case lists:keyfind(vsn, 1, L2) of {vsn, git} -> lists:keyreplace(vsn, 1, L2, {vsn, \"git\"}); _ -> L2 end, \ + ok = file:write_file(AppSrcOut, io_lib:format(\"~p.~n\", [{application, $(1), L3}])) \ +end(), \ +case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end, \ +halt()." +endef + +ifeq ($(V),0) +define dep_autopatch_verbose + @echo " PATCH " $(1); +endef +endif + define dep_fetch if [ "$$$$VS" = "git" ]; then \ git clone -n -- $$$$REPO $(DEPS_DIR)/$(1); \ @@ -135,6 +192,8 @@ define dep_fetch elif [ "$$$$VS" = "hg" ]; then \ hg clone -U $$$$REPO $(DEPS_DIR)/$(1); \ cd $(DEPS_DIR)/$(1) && hg update -q $$$$COMMIT; \ + elif [ "$$$$VS" = "svn" ]; then \ + svn checkout $$$$REPO $(DEPS_DIR)/$(1); \ else \ echo "Unknown or invalid dependency: $(1). Please consult the erlang.mk README for instructions." >&2; \ exit 78; \ @@ -157,6 +216,15 @@ else COMMIT=$(word 3,$(dep_$(1))); \ $(call dep_fetch,$(1)) endif +ifneq ($(filter $(1),$(AUTOPATCH)),) + $(call dep_autopatch_verbose,$(1)) if [ -f $(DEPS_DIR)/$(1)/rebar.config ]; then \ + $(call dep_autopatch,$(1)); \ + cd $(DEPS_DIR)/$(1)/ && ln -s ../../erlang.mk; \ + elif [ ! -f $(DEPS_DIR)/$(1)/Makefile ]; then \ + echo "ERLC_OPTS = +debug_info\ninclude erlang.mk" > $(DEPS_DIR)/$(1)/Makefile; \ + cd $(DEPS_DIR)/$(1)/ && ln -s ../../erlang.mk; \ + fi +endif endef $(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep)))) @@ -199,32 +267,48 @@ help:: " pkg-list List all known packages" \ " pkg-search q=STRING Search for STRING in the package index" -# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-app # Configuration. -ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ - +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec +ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \ + +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec COMPILE_FIRST ?= COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) +ERLC_EXCLUDE ?= +ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) + +ERLC_MIB_OPTS ?= +COMPILE_MIB_FIRST ?= +COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) # Verbosity. appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; appsrc_verbose = $(appsrc_verbose_$(V)) -erlc_verbose_0 = @echo " ERLC " $(filter %.erl %.core,$(?F)); +erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ + $(filter %.erl %.core,$(?F))); erlc_verbose = $(erlc_verbose_$(V)) xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); xyrl_verbose = $(xyrl_verbose_$(V)) -# Core targets. +mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); +mib_verbose = $(mib_verbose_$(V)) + +# Targets. + +ifeq ($(wildcard ebin/test),) +app:: app-build +else +app:: clean app-build +endif -app:: erlc-include ebin/$(PROJECT).app +app-build: erlc-include ebin/$(PROJECT).app $(eval MODULES := $(shell find ebin -type f -name \*.beam \ | sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//')) @if [ -z "$$(grep -E '^[^%]*{modules,' src/$(PROJECT).app.src)" ]; then \ @@ -237,9 +321,15 @@ app:: erlc-include ebin/$(PROJECT).app | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(GITDESCRIBE)\"}/" \ > ebin/$(PROJECT).app +erlc-include: + -@if [ -d ebin/ ]; then \ + find include/ src/ -type f -name \*.hrl -newer ebin -exec touch $(shell find src/ -type f -name "*.erl") \; 2>/dev/null || printf ''; \ + fi + define compile_erl $(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ \ - -pa ebin/ -I include/ $(COMPILE_FIRST_PATHS) $(1) + -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),\ + $(COMPILE_FIRST_PATHS) $(1)) endef define compile_xyrl @@ -248,10 +338,22 @@ define compile_xyrl @rm ebin/*.erl endef +define compile_mib + $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ \ + -I priv/mibs/ $(COMPILE_MIB_FIRST_PATHS) $(1) + $(mib_verbose) erlc -o include/ -- priv/mibs/*.bin +endef + ifneq ($(wildcard src/),) ebin/$(PROJECT).app:: @mkdir -p ebin/ +ifneq ($(wildcard mibs/),) +ebin/$(PROJECT).app:: $(shell find mibs -type f -name \*.mib) + @mkdir -p priv/mibs/ include + $(if $(strip $?),$(call compile_mib,$?)) +endif + ebin/$(PROJECT).app:: $(shell find src -type f -name \*.erl) \ $(shell find src -type f -name \*.core) $(if $(strip $?),$(call compile_erl,$?)) @@ -263,17 +365,56 @@ endif clean:: clean-app -# Extra targets. +clean-app: + $(gen_verbose) rm -rf ebin/ priv/mibs/ \ + $(addprefix include/,$(addsuffix .hrl,$(notdir $(basename $(wildcard mibs/*.mib))))) -erlc-include: - -@if [ -d ebin/ ]; then \ - find include/ src/ -type f -name \*.hrl -newer ebin -exec touch $(shell find src/ -type f -name "*.erl") \; 2>/dev/null || printf ''; \ - fi +# Copyright (c) 2015, Loïc Hoguin <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. -clean-app: - $(gen_verbose) rm -rf ebin/ +.PHONY: test-deps test-dir test-build clean-test-dir + +# Configuration. + +TEST_DIR ?= test + +ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) + +TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard +TEST_ERLC_OPTS += -DTEST=1 + +# Targets. + +$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) -# Copyright (c) 2014, Loïc Hoguin <[email protected]> +test-deps: $(ALL_TEST_DEPS_DIRS) + @for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + +ifneq ($(strip $(TEST_DIR)),) +test-dir: + $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \ + $(wildcard $(TEST_DIR)/*.erl $(TEST_DIR)/*/*.erl) -pa ebin/ +endif + +ifeq ($(wildcard ebin/test),) +test-build: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build: clean deps test-deps + @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" + $(gen_verbose) touch ebin/test +else +test-build: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build: deps test-deps + @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" +endif + +clean:: clean-test-dir + +clean-test-dir: +ifneq ($(wildcard $(TEST_DIR)/*.beam),) + $(gen_verbose) rm -f $(TEST_DIR)/*.beam +endif + +# Copyright (c) 2014-2015, Loïc Hoguin <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates @@ -392,6 +533,56 @@ tpl_gen_server = "-module($(n))." \ "" \ "code_change(_OldVsn, State, _Extra) ->" \ " {ok, State}." +tpl_gen_fsm = "-module($(n))." \ + "-behaviour(gen_fsm)." \ + "" \ + "%% API." \ + "-export([start_link/0])." \ + "" \ + "%% gen_fsm." \ + "-export([init/1])." \ + "-export([state_name/2])." \ + "-export([handle_event/3])." \ + "-export([state_name/3])." \ + "-export([handle_sync_event/4])." \ + "-export([handle_info/3])." \ + "-export([terminate/3])." \ + "-export([code_change/4])." \ + "" \ + "-record(state, {" \ + "})." \ + "" \ + "%% API." \ + "" \ + "-spec start_link() -> {ok, pid()}." \ + "start_link() ->" \ + " gen_fsm:start_link(?MODULE, [], [])." \ + "" \ + "%% gen_fsm." \ + "" \ + "init([]) ->" \ + " {ok, state_name, \#state{}}." \ + "" \ + "state_name(_Event, StateData) ->" \ + " {next_state, state_name, StateData}." \ + "" \ + "handle_event(_Event, StateName, StateData) ->" \ + " {next_state, StateName, StateData}." \ + "" \ + "state_name(_Event, _From, StateData) ->" \ + " {reply, ignored, state_name, StateData}." \ + "" \ + "handle_sync_event(_Event, _From, StateName, StateData) ->" \ + " {reply, ignored, StateName, StateData}." \ + "" \ + "handle_info(_Info, StateName, StateData) ->" \ + " {next_state, StateName, StateData}." \ + "" \ + "terminate(_Reason, _StateName, _StateData) ->" \ + " ok." \ + "" \ + "code_change(_OldVsn, StateName, StateData, _Extra) ->" \ + " {ok, StateName, StateData}." tpl_cowboy_http = "-module($(n))." \ "-behaviour(cowboy_http_handler)." \ "" \ @@ -551,85 +742,169 @@ endif list-templates: @echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) -# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +# Copyright (c) 2014-2015, Loïc Hoguin <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: build-ct-deps build-ct-suites tests-ct clean-ct distclean-ct +.PHONY: clean-c_src distclean-c_src-env +# todo + +# Configuration. + +C_SRC_DIR = $(CURDIR)/c_src +C_SRC_ENV ?= $(C_SRC_DIR)/env.mk +C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT).so + +# System type and C compiler/flags. + +UNAME_SYS := $(shell uname -s) +ifeq ($(UNAME_SYS), Darwin) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall + LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress +else ifeq ($(UNAME_SYS), FreeBSD) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +else ifeq ($(UNAME_SYS), Linux) + CC ?= gcc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +endif + +CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) +CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) + +LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -lerl_interface -lei +LDFLAGS += -shared + +# Verbosity. + +c_verbose_0 = @echo " C " $(?F); +c_verbose = $(c_verbose_$(V)) + +cpp_verbose_0 = @echo " CPP " $(?F); +cpp_verbose = $(cpp_verbose_$(V)) + +link_verbose_0 = @echo " LD " $(@F); +link_verbose = $(link_verbose_$(V)) + +# Targets. + +ifeq ($(wildcard $(C_SRC_DIR)),) +else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),) +app:: + $(MAKE) -C $(C_SRC_DIR) + +clean:: + $(MAKE) -C $(C_SRC_DIR) clean + +else +SOURCES := $(shell find $(C_SRC_DIR) -type f \( -name "*.c" -o -name "*.C" -o -name "*.cc" -o -name "*.cpp" \)) +OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) + +COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c +COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c + +app:: $(C_SRC_ENV) $(C_SRC_OUTPUT) + +$(C_SRC_OUTPUT): $(OBJECTS) + @mkdir -p priv/ + $(link_verbose) $(CC) $(OBJECTS) $(LDFLAGS) $(LDLIBS) -o $(C_SRC_OUTPUT) + +%.o: %.c + $(COMPILE_C) $(OUTPUT_OPTION) $< + +%.o: %.cc + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.C + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.cpp + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +$(C_SRC_ENV): + @$(ERL) -eval "file:write_file(\"$(C_SRC_ENV)\", \ + io_lib:format( \ + \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \ + \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \ + \"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \ + [code:root_dir(), erlang:system_info(version), \ + code:lib_dir(erl_interface, include), \ + code:lib_dir(erl_interface, lib)])), \ + halt()." + +clean:: clean-c_src + +clean-c_src: + $(gen_verbose) rm -f $(C_SRC_OUTPUT) $(OBJECTS) + +distclean:: distclean-c_src-env + +distclean-c_src-env: + $(gen_verbose) rm -f $(C_SRC_ENV) + +-include $(C_SRC_ENV) +endif + +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: ct distclean-ct # Configuration. CT_OPTS ?= -ifneq ($(wildcard test/),) - CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(shell find test -type f -name \*_SUITE.erl -exec basename {} \;))) +ifneq ($(wildcard $(TEST_DIR)),) + CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(shell find $(TEST_DIR) -type f -name \*_SUITE.erl -exec basename {} \;))) else CT_SUITES ?= endif -TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard -TEST_ERLC_OPTS += -DTEST=1 -DEXTRA=1 +'{parse_transform, eunit_autoexport}' - # Core targets. -tests:: tests-ct - -clean:: clean-ct +tests:: ct distclean:: distclean-ct help:: @printf "%s\n" "" \ + "Common_test targets:" \ + " ct Run all the common_test suites for this project" \ + "" \ "All your common_test suites have their associated targets." \ "A suite named http_SUITE can be ran using the ct-http target." # Plugin-specific targets. -ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) - CT_RUN = ct_run \ -no_auto_compile \ - -noshell \ - -pa $(realpath ebin) $(DEPS_DIR)/*/ebin \ - -dir test \ + -noinput \ + -pa ebin $(DEPS_DIR)/*/ebin \ + -dir $(TEST_DIR) \ -logdir logs -$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) - -build-ct-deps: $(ALL_TEST_DEPS_DIRS) - @for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done - -build-ct-suites: build-ct-deps - $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o test/ \ - $(wildcard test/*.erl test/*/*.erl) -pa ebin/ - -tests-ct: ERLC_OPTS = $(TEST_ERLC_OPTS) -tests-ct: clean deps app build-ct-suites - @if [ -d "test" ] ; \ - then \ - mkdir -p logs/ ; \ - $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) ; \ - fi - $(gen_verbose) rm -f test/*.beam +ifeq ($(CT_SUITES),) +ct: +else +ct: test-build + @mkdir -p logs/ + $(gen_verbose) $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) +endif define ct_suite_target -ct-$(1): ERLC_OPTS = $(TEST_ERLC_OPTS) -ct-$(1): clean deps app build-ct-suites - @if [ -d "test" ] ; \ - then \ - mkdir -p logs/ ; \ - $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) $(CT_OPTS) ; \ - fi - $(gen_verbose) rm -f test/*.beam +ct-$(1): test-build + @mkdir -p logs/ + $(gen_verbose) $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) $(CT_OPTS) endef $(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) -clean-ct: - $(gen_verbose) rm -rf test/*.beam - distclean-ct: $(gen_verbose) rm -rf logs/ -# Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: plt distclean-plt dialyze @@ -671,6 +946,35 @@ dialyze: $(DIALYZER_PLT) endif @dialyzer --no_native $(DIALYZER_DIRS) $(DIALYZER_OPTS) +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> +# Copyright (c) 2015, Viktor Söderqvist <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-edoc build-doc-deps + +# Configuration. + +EDOC_OPTS ?= + +# Core targets. + +docs:: distclean-edoc build-doc-deps + $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' + +distclean:: distclean-edoc + +# Plugin-specific targets. + +DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS)) + +$(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep)))) + +build-doc-deps: $(DOC_DEPS_DIRS) + @for dep in $(DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + +distclean-edoc: + $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info + # Copyright (c) 2014, Juan Facorro <[email protected]> # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -694,25 +998,221 @@ help:: "Elvis targets:" \ " elvis Run Elvis using the local elvis.config or download the default otherwise" -ifneq ($(wildcard $(ELVIS_CONFIG)),) -rel:: distclean-elvis -endif - distclean:: distclean-elvis # Plugin-specific targets. $(ELVIS): - @$(call core_http_get,$(ELVIS_CONFIG),$(ELVIS_CONFIG_URL)) @$(call core_http_get,$(ELVIS),$(ELVIS_URL)) @chmod +x $(ELVIS) -elvis: $(ELVIS) +$(ELVIS_CONFIG): + @$(call core_http_get,$(ELVIS_CONFIG),$(ELVIS_CONFIG_URL)) + +elvis: $(ELVIS) $(ELVIS_CONFIG) @$(ELVIS) rock -c $(ELVIS_CONFIG) $(ELVIS_OPTS) distclean-elvis: $(gen_verbose) rm -rf $(ELVIS) +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Configuration. + +DTL_FULL_PATH ?= 0 + +# Verbosity. + +dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); +dtl_verbose = $(dtl_verbose_$(V)) + +# Core targets. + +define compile_erlydtl + $(dtl_verbose) $(ERL) -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \ + Compile = fun(F) -> \ + S = fun (1) -> re:replace(filename:rootname(string:sub_string(F, 11), ".dtl"), "/", "_", [{return, list}, global]); \ + (0) -> filename:basename(F, ".dtl") \ + end, \ + Module = list_to_atom(string:to_lower(S($(DTL_FULL_PATH))) ++ "_dtl"), \ + {ok, _} = erlydtl:compile(F, Module, [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) \ + end, \ + _ = [Compile(F) || F <- string:tokens("$(1)", " ")], \ + halt().' +endef + +ifneq ($(wildcard src/),) +ebin/$(PROJECT).app:: $(shell find templates -type f -name \*.dtl 2>/dev/null) + $(if $(strip $?),$(call compile_erlydtl,$?)) +endif + +# Copyright (c) 2014 Dave Cottlehuber <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-escript escript + +# Configuration. + +ESCRIPT_NAME ?= $(PROJECT) +ESCRIPT_COMMENT ?= This is an -*- erlang -*- file + +ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" +ESCRIPT_SYS_CONFIG ?= "rel/sys.config" +ESCRIPT_EMU_ARGS ?= -pa . \ + -sasl errlog_type error \ + -escript main $(ESCRIPT_NAME) +ESCRIPT_SHEBANG ?= /usr/bin/env escript +ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" + +# Core targets. + +distclean:: distclean-escript + +help:: + @printf "%s\n" "" \ + "Escript targets:" \ + " escript Build an executable escript archive" \ + +# Plugin-specific targets. + +# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl +# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center +# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : +# Software may only be used for the great good and the true happiness of all +# sentient beings. + +define ESCRIPT_RAW +'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ +'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ +' [F || F <- A, not filelib:is_dir(F) ] end,'\ +'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ +'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ +'Ez = fun(Escript) ->'\ +' Static = Files([$(ESCRIPT_STATIC)]),'\ +' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ +' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ +' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ +' {archive, Archive, [memory]},'\ +' {shebang, "$(ESCRIPT_SHEBANG)"},'\ +' {comment, "$(ESCRIPT_COMMENT)"},'\ +' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ +' ]),'\ +' file:change_mode(Escript, 8#755)'\ +'end,'\ +'Ez("$(ESCRIPT_NAME)"),'\ +'halt().' +endef + +ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) + +escript:: distclean-escript deps app + $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) + +distclean-escript: + $(gen_verbose) rm -f $(ESCRIPT_NAME) + +# Copyright (c) 2014, Enrique Fernandez <[email protected]> +# Copyright (c) 2015, Loïc Hoguin <[email protected]> +# This file is contributed to erlang.mk and subject to the terms of the ISC License. + +.PHONY: eunit + +# Configuration + +ifeq ($(strip $(TEST_DIR)),) +TAGGED_EUNIT_TESTS = {dir,"ebin"} +else +ifeq ($(wildcard $(TEST_DIR)),) +TAGGED_EUNIT_TESTS = {dir,"ebin"} +else +# All modules in TEST_DIR +TEST_DIR_MODS = $(notdir $(basename $(shell find $(TEST_DIR) -type f -name *.beam))) +# All modules in 'ebin' +EUNIT_EBIN_MODS = $(notdir $(basename $(shell find ebin -type f -name *.beam))) +# Only those modules in TEST_DIR with no matching module in 'ebin'. +# This is done to avoid some tests being executed twice. +EUNIT_MODS = $(filter-out $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(TEST_DIR_MODS)) +TAGGED_EUNIT_TESTS = {dir,"ebin"} $(foreach mod,$(EUNIT_MODS),$(shell echo $(mod) | sed -e 's/\(.*\)/{module,\1}/g')) +endif +endif + +EUNIT_OPTS ?= verbose + +# Utility functions + +define str-join + $(shell echo '$(strip $(1))' | sed -e "s/ /,/g") +endef + +# Core targets. + +tests:: eunit + +help:: + @printf "%s\n" "" \ + "EUnit targets:" \ + " eunit Run all the EUnit tests for this project" + +# Plugin-specific targets. + +EUNIT_RUN = $(ERL) \ + -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin \ + -pz ebin \ + -eval 'case eunit:test([$(call str-join,$(TAGGED_EUNIT_TESTS))], [$(EUNIT_OPTS)]) of ok -> halt(0); error -> halt(1) end.' + +eunit: test-build + $(gen_verbose) $(EUNIT_RUN) + +# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: relx-rel distclean-relx-rel distclean-relx + +# Configuration. + +RELX_CONFIG ?= $(CURDIR)/relx.config + +RELX ?= $(CURDIR)/relx +export RELX + +RELX_URL ?= https://github.com/erlware/relx/releases/download/v1.2.0/relx +RELX_OPTS ?= +RELX_OUTPUT_DIR ?= _rel + +ifeq ($(firstword $(RELX_OPTS)),-o) + RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) +else + RELX_OPTS += -o $(RELX_OUTPUT_DIR) +endif + +# Core targets. + +ifneq ($(wildcard $(RELX_CONFIG)),) +rel:: distclean-relx-rel relx-rel +endif + +distclean:: distclean-relx-rel distclean-relx + +# Plugin-specific targets. + +define relx_fetch + $(call core_http_get,$(RELX),$(RELX_URL)) + chmod +x $(RELX) +endef + +$(RELX): + @$(call relx_fetch) + +relx-rel: $(RELX) + @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) + +distclean-relx-rel: + $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) + +distclean-relx: + $(gen_verbose) rm -rf $(RELX) + # Copyright (c) 2014, M Robert Martin <[email protected]> # This file is contributed to erlang.mk and subject to the terms of the ISC License. @@ -720,7 +1220,7 @@ distclean-elvis: # Configuration. -SHELL_PATH ?= -pa ../$(PROJECT)/ebin $(DEPS_DIR)/*/ebin +SHELL_PATH ?= -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin SHELL_OPTS ?= ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS)) @@ -741,3 +1241,35 @@ build-shell-deps: $(ALL_SHELL_DEPS_DIRS) shell: build-shell-deps $(gen_verbose) erl $(SHELL_PATH) $(SHELL_OPTS) + +# Copyright (c) 2015, Loïc Hoguin <[email protected]> +# This file is part of erlang.mk and subject to the terms of the ISC License. + +ifneq ($(wildcard $(DEPS_DIR)/triq),) +.PHONY: triq + +# Targets. + +tests:: triq + +define triq_run +$(ERL) -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin \ + -eval "try $(1) of true -> halt(0); _ -> halt(1) catch error:undef -> io:format(\"Undefined property or module~n\"), halt() end." +endef + +ifdef t +ifeq (,$(findstring :,$(t))) +triq: test-build + @$(call triq_run,triq:check($(t))) +else +triq: test-build + @echo Testing $(t)/0 + @$(call triq_run,triq:check($(t)())) +endif +else +triq: test-build + $(eval MODULES := $(shell find ebin -type f -name \*.beam \ + | sed "s/ebin\//'/;s/\.beam/',/" | sed '$$s/.$$//')) + $(gen_verbose) $(call triq_run,[true] =:= lists:usort([triq:check(M) || M <- [$(MODULES)]])) +endif +endif diff --git a/include/cow_inline.hrl b/include/cow_inline.hrl index 458e011..5c43a5a 100644 --- a/include/cow_inline.hrl +++ b/include/cow_inline.hrl @@ -1,4 +1,4 @@ -%% Copyright (c) 2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2014-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above diff --git a/src/cow_cookie.erl b/src/cow_cookie.erl index 02df65e..150efeb 100644 --- a/src/cow_cookie.erl +++ b/src/cow_cookie.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above diff --git a/src/cow_date.erl b/src/cow_date.erl index 1e54090..b805aec 100644 --- a/src/cow_date.erl +++ b/src/cow_date.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -182,9 +182,7 @@ http_date_test_() -> {<<"Sun Nov 6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}} ], [{V, fun() -> R = http_date(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_http_date_fixdate() -> horse:repeat(200000, http_date(<<"Sun, 06 Nov 1994 08:49:37 GMT">>) @@ -221,9 +219,7 @@ rfc2109_test_() -> {<<"Sun, 01-Jan-2012 00:00:00 GMT">>, {{2012, 1, 1}, { 0, 0, 0}}} ], [{R, fun() -> R = rfc2109(D) end} || {R, D} <- Tests]. --endif. --ifdef(PERF). horse_rfc2019_20130101_000000() -> horse:repeat(100000, rfc2109({{2013, 1, 1}, {0, 0, 0}}) diff --git a/src/cow_http.erl b/src/cow_http.erl index 60f3faa..8f2ae92 100644 --- a/src/cow_http.erl +++ b/src/cow_http.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -82,9 +82,7 @@ parse_status_line_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_status_line(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_status_line_200() -> horse:repeat(200000, parse_status_line(<<"HTTP/1.1 200 OK\r\n">>) @@ -184,9 +182,7 @@ parse_headers_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_headers(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_headers() -> horse:repeat(50000, parse_headers(<<"Server: Erlang/R17\r\n" diff --git a/src/cow_http_hd.erl b/src/cow_http_hd.erl index b54ade6..e47d80d 100644 --- a/src/cow_http_hd.erl +++ b/src/cow_http_hd.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2014-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -386,9 +386,7 @@ parse_accept_error_test_() -> <<"audio/basic;t=\"zero \\", 0, " woo\"">> ], [{V, fun() -> {'EXIT', _} = (catch parse_accept(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_accept() -> horse:repeat(20000, parse_accept(<<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " @@ -480,9 +478,7 @@ parse_accept_charset_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_accept_charset(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_accept_charset() -> horse:repeat(20000, parse_accept_charset(<<"iso-8859-5, unicode-1-1;q=0.8">>) @@ -538,9 +534,7 @@ parse_accept_encoding_test_() -> ]} ], [{V, fun() -> R = parse_accept_encoding(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_accept_encoding() -> horse:repeat(20000, parse_accept_encoding(<<"gzip;q=1.0, identity; q=0.5, *;q=0">>) @@ -657,9 +651,7 @@ parse_accept_language_error_test_() -> <<"419-en-us">> ], [{V, fun() -> {'EXIT', _} = (catch parse_accept_language(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_accept_language() -> horse:repeat(20000, parse_accept_language(<<"da, en-gb;q=0.8, en;q=0.7">>) @@ -688,9 +680,7 @@ parse_accept_ranges_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_accept_ranges(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_accept_ranges_none() -> horse:repeat(200000, parse_accept_ranges(<<"none">>) @@ -764,9 +754,7 @@ parse_allow_test_() -> {<<"GET, HEAD, PUT">>, [<<"GET">>, <<"HEAD">>, <<"PUT">>]} ], [{V, fun() -> R = parse_allow(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_allow() -> horse:repeat(200000, parse_allow(<<"GET, HEAD, PUT">>) @@ -863,9 +851,7 @@ parse_authorization_test_() -> {<<"opaque">>, <<"5ccc069c403ebaf9f0171e9517f40e41">>}]}} ], [{V, fun() -> R = parse_authorization(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_authorization_basic() -> horse:repeat(20000, parse_authorization(<<"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==">>) @@ -1014,9 +1000,7 @@ parse_cache_control_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_cache_control(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_cache_control_no_cache() -> horse:repeat(200000, parse_cache_control(<<"no-cache">>) @@ -1078,9 +1062,7 @@ parse_connection_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_connection(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_connection_close() -> horse:repeat(200000, parse_connection(<<"close">>) @@ -1115,9 +1097,7 @@ parse_content_encoding_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_content_encoding(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_content_encoding() -> horse:repeat(200000, parse_content_encoding(<<"gzip">>) @@ -1403,9 +1383,7 @@ parse_content_language_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_content_language(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_content_language() -> horse:repeat(100000, parse_content_language(<<"fr, en-US, es-419, az-Arab, x-pig-latin, man-Nkoo-GN">>) @@ -1449,9 +1427,7 @@ parse_content_length_error_test_() -> <<"4.17">> ], [{V, fun() -> {'EXIT', _} = (catch parse_content_length(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_content_length_zero() -> horse:repeat(100000, parse_content_length(<<"0">>) @@ -1551,9 +1527,7 @@ parse_content_range_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_content_range(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_content_range_bytes() -> horse:repeat(200000, parse_content_range(<<"bytes 21010-47021/47022">>) @@ -1666,9 +1640,7 @@ parse_content_type_test_() -> ]}} ], [{V, fun() -> R = parse_content_type(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_content_type() -> horse:repeat(200000, parse_content_type(<<"text/html;charset=utf-8">>) @@ -1739,9 +1711,7 @@ parse_etag_error_test_() -> <<"W/">> ], [{V, fun() -> {'EXIT', _} = (catch parse_etag(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_etag() -> horse:repeat(200000, parse_etag(<<"W/\"xyzzy\"">>) @@ -1789,9 +1759,7 @@ parse_expect_error_test_() -> <<"Cookies">> ], [{V, fun() -> {'EXIT', _} = (catch parse_expect(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_expect() -> horse:repeat(200000, parse_expect(<<"100-continue">>) @@ -1821,9 +1789,7 @@ parse_expires_test_() -> {<<"Thu, 01 Dec 1994 16:00:00 GMT">>, {{1994, 12, 1}, {16, 0, 0}}} ], [{V, fun() -> R = parse_expires(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_expires_0() -> horse:repeat(200000, parse_expires(<<"0">>) @@ -1897,9 +1863,7 @@ parse_host_test_() -> {<<"[::ffff:192.0.2.1]">>, {<<"[::ffff:192.0.2.1]">>, undefined}} ], [{V, fun() -> R = parse_host(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_host_blue_example_org() -> horse:repeat(200000, parse_host(<<"blue.example.org:8080">>) @@ -1966,9 +1930,7 @@ parse_if_match_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_if_match(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_if_match() -> horse:repeat(200000, parse_if_match(<<"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\"">>) @@ -2015,9 +1977,7 @@ parse_if_none_match_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_if_none_match(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_if_none_match() -> horse:repeat(200000, parse_if_none_match(<<"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\"">>) @@ -2048,9 +2008,7 @@ parse_if_range_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_if_range(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_if_range_etag() -> horse:repeat(200000, parse_if_range(<<"\"xyzzy\"">>) @@ -2255,9 +2213,7 @@ parse_range_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_range(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_range_first_last() -> horse:repeat(200000, parse_range(<<"bytes=500-999">>) @@ -2307,9 +2263,7 @@ parse_retry_after_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_retry_after(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_retry_after_date() -> horse:repeat(200000, parse_retry_after(<<"Fri, 31 Dec 1999 23:59:59 GMT">>) @@ -2417,9 +2371,7 @@ parse_sec_websocket_extensions_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_sec_websocket_extensions(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_sec_websocket_extensions() -> horse:repeat(200000, parse_sec_websocket_extensions(<<"mux; max-channels=4; flow-control, deflate-stream">>) @@ -2456,9 +2408,7 @@ parse_sec_websocket_protocol_req_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_sec_websocket_protocol_req(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_sec_websocket_protocol_req() -> horse:repeat(200000, parse_sec_websocket_protocol_req(<<"chat, superchat">>) @@ -2494,9 +2444,7 @@ parse_sec_websocket_protocol_resp_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_sec_websocket_protocol_resp(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_sec_websocket_protocol_resp() -> horse:repeat(200000, parse_sec_websocket_protocol_resp(<<"chat">>) @@ -2533,9 +2481,7 @@ parse_sec_websocket_version_req_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_sec_websocket_version_req(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_sec_websocket_version_req_13() -> horse:repeat(200000, parse_sec_websocket_version_req(<<"13">>) @@ -2591,9 +2537,7 @@ parse_sec_websocket_version_resp_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_sec_websocket_version_resp(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_sec_websocket_version_resp() -> horse:repeat(200000, parse_sec_websocket_version_resp(<<"13, 8, 7">>) @@ -2691,9 +2635,7 @@ parse_te_test_() -> {<<"trailers, deflate;q=0.5">>, {trailers, [{<<"deflate">>, 500}]}} ], [{V, fun() -> R = parse_te(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_te() -> horse:repeat(200000, parse_te(<<"trailers, deflate;q=0.5">>) @@ -2718,9 +2660,7 @@ parse_trailer_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_trailer(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_trailer() -> horse:repeat(200000, parse_trailer(<<"Date, Content-MD5">>) @@ -2769,9 +2709,7 @@ parse_transfer_encoding_error_test_() -> ], [{V, fun() -> {'EXIT', _} = (catch parse_transfer_encoding(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_transfer_encoding_chunked() -> horse:repeat(200000, parse_transfer_encoding(<<"chunked">>) @@ -3018,9 +2956,7 @@ parse_www_authenticate_error_test_() -> <<>> ], [{V, fun() -> {'EXIT', _} = (catch parse_www_authenticate(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_www_authenticate() -> horse:repeat(200000, parse_www_authenticate(<<"Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"">>) diff --git a/src/cow_http_te.erl b/src/cow_http_te.erl index 5ab71f4..1e7b43f 100644 --- a/src/cow_http_te.erl +++ b/src/cow_http_te.erl @@ -34,7 +34,7 @@ | {done, Data::binary(), TotalLen::non_neg_integer(), Rest::binary()}. -export_type([decode_ret/0]). --ifdef(EXTRA). +-ifdef(TEST). dripfeed(<< C, Rest/bits >>, Acc, State, F) -> case F(<< Acc/binary, C >>, State) of more -> @@ -92,11 +92,8 @@ stream_identity_parts_test() -> {done, << 0:7992 >>, 2999, <<>>} = stream_identity(<< 0:7992 >>, S2), ok. --endif. --ifdef(PERF). %% Using the same data as the chunked one for comparison. - horse_stream_identity() -> horse:repeat(10000, stream_identity(<< @@ -296,9 +293,7 @@ stream_chunked_error_test_() -> [{lists:flatten(io_lib:format("value ~p state ~p", [V, S])), fun() -> {'EXIT', _} = (catch stream_chunked(V, S)) end} || {V, S} <- Tests]. --endif. --ifdef(PERF). horse_stream_chunked() -> horse:repeat(10000, stream_chunked(<< diff --git a/src/cow_mimetypes.erl b/src/cow_mimetypes.erl index 69284e1..58585b9 100644 --- a/src/cow_mimetypes.erl +++ b/src/cow_mimetypes.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above diff --git a/src/cow_multipart.erl b/src/cow_multipart.erl index 284c597..276a689 100644 --- a/src/cow_multipart.erl +++ b/src/cow_multipart.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2014-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -98,7 +98,7 @@ >>). -define(TEST4_BOUNDARY, <<"boundary">>). -%% RFC 2046, Section 5.1.1: +%% RFC 2046, Section 5.1.1 -define(TEST5_MIME, << "This is the preamble. It is to be ignored, though it\r\n" "is a handy place for composition agents to include an\r\n" @@ -203,8 +203,7 @@ skip_preamble(Stream, Boundary) -> end. before_parse_headers(<< "\r\n\r\n", Stream/bits >>) -> - %% This indicates that there are no headers, so we can abort - %% immediately. + %% This indicates that there are no headers, so we can abort immediately. {ok, [], Stream}; before_parse_headers(<< "\r\n", Stream/bits >>) -> %% There is a line break right after the boundary, skip it. @@ -364,7 +363,7 @@ parse_epilogue_crlf_test() -> ok. parse_rfc2046_test() -> - %% The following is an included in RFC 2046, Section 5.1.1. + %% The following is an example included in RFC 2046, Section 5.1.1. Body1 = <<"This is implicitly typed plain US-ASCII text.\r\n" "It does NOT end with a linebreak.">>, Body2 = <<"This is explicitly typed plain US-ASCII text.\r\n" @@ -400,9 +399,7 @@ parse_partial_test() -> {ok, <<"boundary">>, <<"\r\n--">>} = parse_body(<<"boundary\r\n--">>, <<"boundary">>), ok. --endif. --ifdef(PERF). perf_parse_multipart(Stream, Boundary) -> case parse_headers(Stream, Boundary) of {ok, _, Rest} -> @@ -499,9 +496,7 @@ identity_test() -> {done, Body2, M6} = parse_body(M5, B), {done, Epilogue} = parse_headers(M6, B), ok. --endif. --ifdef(PERF). perf_build_multipart() -> B = boundary(), [ @@ -613,9 +608,7 @@ parse_content_disposition_test_() -> {<<"file">>, [{<<"filename">>, <<"file2.gif">>}]}} ], [{V, fun() -> R = parse_content_disposition(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_content_disposition_attachment() -> horse:repeat(100000, parse_content_disposition(<<"attachment; filename=genome.jpeg;" @@ -655,9 +648,7 @@ parse_content_transfer_encoding_test_() -> ], [{V, fun() -> R = parse_content_transfer_encoding(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_content_transfer_encoding() -> horse:repeat(100000, parse_content_transfer_encoding(<<"QUOTED-PRINTABLE">>) @@ -718,9 +709,7 @@ parse_content_type_test_() -> ], [{V, fun() -> R = parse_content_type(V) end} || {V, R} <- Tests]. --endif. --ifdef(PERF). horse_parse_content_type_zero() -> horse:repeat(100000, parse_content_type(<<"text/plain">>) diff --git a/src/cow_qs.erl b/src/cow_qs.erl index 413562b..33d385b 100644 --- a/src/cow_qs.erl +++ b/src/cow_qs.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -126,9 +126,7 @@ parse_qs_identity_test_() -> "b-sid=521732&ortb-xt=IAB3&ortb-ugc=">> ], [{V, fun() -> V = qs(parse_qs(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_parse_qs_shorter() -> horse:repeat(20000, parse_qs(<<"hl=en&q=erlang%20cowboy">>) @@ -310,9 +308,7 @@ qs_identity_test_() -> [{lists:flatten(io_lib:format("~p", [V])), fun() -> V = parse_qs(qs(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_qs_shorter() -> horse:repeat(20000, qs(?QS_SHORTER)). @@ -395,9 +391,7 @@ urldecode_identity_test_() -> "%BE%8B%E3%80%9C">> ], [{V, fun() -> V = urlencode(urldecode(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_urldecode() -> horse:repeat(100000, urldecode(<<"nothingnothingnothingnothing">>) @@ -544,9 +538,7 @@ urlencode_identity_test_() -> 129,153,227,130,139,230,151,139,229,190,139,227,128,156>> ], [{V, fun() -> V = urldecode(urlencode(V)) end} || V <- Tests]. --endif. --ifdef(PERF). horse_urlencode() -> horse:repeat(100000, urlencode(<<"nothingnothingnothingnothing">>) diff --git a/src/cow_spdy.erl b/src/cow_spdy.erl index 59c1ba4..ac7f0fc 100644 --- a/src/cow_spdy.erl +++ b/src/cow_spdy.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above diff --git a/src/cow_ws.erl b/src/cow_ws.erl index de7d0b5..c89c17a 100644 --- a/src/cow_ws.erl +++ b/src/cow_ws.erl @@ -14,27 +14,206 @@ -module(cow_ws). +-export([key/0]). +-export([encode_key/1]). + +-export([negotiate_permessage_deflate/3]). +-export([negotiate_x_webkit_deflate_frame/3]). + +-export([validate_permessage_deflate/3]). + -export([parse_header/3]). --export([parse_close_code/2]). -export([parse_payload/9]). +-export([make_frame/4]). + -export([frame/2]). +-export([masked_frame/2]). -type close_code() :: 1000..1003 | 1006..1011 | 3000..4999. -export_type([close_code/0]). --type frag_state() :: undefined | {fin | nofin, text | binary}. +-type extensions() :: map(). +-export_type([extensions/0]). + +-type frag_state() :: undefined | {fin | nofin, text | binary, rsv()}. -export_type([frag_state/0]). -type frame() :: close | ping | pong | {text | binary | close | ping | pong, iodata()} - | {close, close_code(), iodata()}. + | {close, close_code(), iodata()} + | {fragment, fin | nofin, text | binary | continuation, iodata()}. -export_type([frame/0]). --type extensions() :: map(). -type frame_type() :: fragment | text | binary | close | ping | pong. +-export_type([frame_type/0]). + -type mask_key() :: undefined | 0..16#ffffffff. +-export_type([mask_key/0]). + -type rsv() :: <<_:3>>. --type utf8_state() :: <<>> | <<_:8>> | <<_:16>> | <<_:24>>. +-export_type([rsv/0]). + +-type utf8_state() :: 0..8. +-export_type([utf8_state/0]). + +%% @doc Generate a key for the Websocket handshake request. + +-spec key() -> binary(). +key() -> + base64:encode(crypto:rand_bytes(16)). + +%% @doc Encode the key into the accept value for the Websocket handshake response. + +-spec encode_key(binary()) -> binary(). +encode_key(Key) -> + base64:encode(crypto:hash(sha, [Key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"])). + +%% @doc Negotiate the permessage-deflate extension. + +%% Ignore if deflate already negotiated. +negotiate_permessage_deflate(_, #{deflate := _}, _) -> + ignore; +negotiate_permessage_deflate(Params, Extensions, Opts) -> + case lists:usort(Params) of + %% Ignore if multiple parameters with the same name. + Params2 when length(Params) =/= length(Params2) -> + ignore; + Params2 -> + %% @todo Might want to make these configurable defaults. + case parse_request_permessage_deflate_params(Params2, 15, takeover, 15, takeover, []) of + ignore -> + ignore; + {ClientWindowBits, ClientTakeOver, ServerWindowBits, ServerTakeOver, RespParams} -> + {Inflate, Deflate} = init_permessage_deflate(ClientWindowBits, ServerWindowBits, Opts), + {ok, [<<"permessage-deflate">>, RespParams], + Extensions#{ + deflate => Deflate, + deflate_takeover => ServerTakeOver, + inflate => Inflate, + inflate_takeover => ClientTakeOver}} + end + end. + +parse_request_permessage_deflate_params([], CB, CTO, SB, STO, RespParams) -> + {CB, CTO, SB, STO, RespParams}; +parse_request_permessage_deflate_params([<<"client_max_window_bits">>|Tail], CB, CTO, SB, STO, RespParams) -> + parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, + [<<"; ">>, <<"client_max_window_bits=">>, integer_to_binary(CB)|RespParams]); +parse_request_permessage_deflate_params([{<<"client_max_window_bits">>, Max}|Tail], _, CTO, SB, STO, RespParams) -> + case parse_max_window_bits(Max) of + error -> + ignore; + CB -> + parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, + [<<"; ">>, <<"client_max_window_bits=">>, Max|RespParams]) + end; +parse_request_permessage_deflate_params([<<"client_no_context_takeover">>|Tail], CB, _, SB, STO, RespParams) -> + parse_request_permessage_deflate_params(Tail, CB, no_takeover, SB, STO, [<<"; ">>, <<"client_no_context_takeover">>|RespParams]); +parse_request_permessage_deflate_params([{<<"server_max_window_bits">>, Max}|Tail], CB, CTO, _, STO, RespParams) -> + case parse_max_window_bits(Max) of + error -> + ignore; + SB -> + parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, + [<<"; ">>, <<"server_max_window_bits=">>, Max|RespParams]) + end; +parse_request_permessage_deflate_params([<<"server_no_context_takeover">>|Tail], CB, CTO, SB, _, RespParams) -> + parse_request_permessage_deflate_params(Tail, CB, CTO, SB, no_takeover, [<<"; ">>, <<"server_no_context_takeover">>|RespParams]); +%% Ignore if unknown parameter; ignore if parameter with invalid or missing value. +parse_request_permessage_deflate_params(_, _, _, _, _, _) -> + ignore. + +parse_max_window_bits(<<"8">>) -> 8; +parse_max_window_bits(<<"9">>) -> 9; +parse_max_window_bits(<<"10">>) -> 10; +parse_max_window_bits(<<"11">>) -> 11; +parse_max_window_bits(<<"12">>) -> 12; +parse_max_window_bits(<<"13">>) -> 13; +parse_max_window_bits(<<"14">>) -> 14; +parse_max_window_bits(<<"15">>) -> 15; +parse_max_window_bits(_) -> error. + +% A negative WindowBits value indicates that zlib headers are not used. +init_permessage_deflate(InflateWindowBits, DeflateWindowBits, Opts) -> + Inflate = zlib:open(), + ok = zlib:inflateInit(Inflate, -InflateWindowBits), + Deflate = zlib:open(), + %% @todo Remove this case .. of for OTP 18+ if PR https://github.com/erlang/otp/pull/633 gets merged. + DeflateWindowBits2 = case DeflateWindowBits of + 8 -> 9; + _ -> DeflateWindowBits + end, + ok = zlib:deflateInit(Deflate, + maps:get(level, Opts, best_compression), + deflated, + -DeflateWindowBits2, + maps:get(mem_level, Opts, 8), + maps:get(strategy, Opts, default)), + {Inflate, Deflate}. + +%% @doc Negotiate the x-webkit-deflate-frame extension. +%% +%% The implementation is very basic and none of the parameters +%% are currently supported. + +negotiate_x_webkit_deflate_frame(_, #{deflate := _}, _) -> + ignore; +negotiate_x_webkit_deflate_frame(_Params, Extensions, Opts) -> + % Since we are negotiating an unconstrained deflate-frame + % then we must be willing to accept frames using the + % maximum window size which is 2^15. + {Inflate, Deflate} = init_permessage_deflate(15, 15, Opts), + {ok, <<"x-webkit-deflate-frame">>, + Extensions#{ + deflate => Deflate, + deflate_takeover => takeover, + inflate => Inflate, + inflate_takeover => takeover}}. + +%% @doc Validate the negotiated permessage-deflate extension. + +%% Error when more than one deflate extension was negotiated. +validate_permessage_deflate(_, #{deflate := _}, _) -> + error; +validate_permessage_deflate(Params, Extensions, Opts) -> + case lists:usort(Params) of + %% Error if multiple parameters with the same name. + Params2 when length(Params) =/= length(Params2) -> + error; + Params2 -> + %% @todo Might want to make some of these configurable defaults if at all possible. + case parse_response_permessage_deflate_params(Params2, 15, takeover, 15, takeover) of + error -> + error; + {ClientWindowBits, ClientTakeOver, ServerWindowBits, ServerTakeOver} -> + {Inflate, Deflate} = init_permessage_deflate(ServerWindowBits, ClientWindowBits, Opts), + {ok, Extensions#{ + deflate => Deflate, + deflate_takeover => ClientTakeOver, + inflate => Inflate, + inflate_takeover => ServerTakeOver}} + end + end. + +parse_response_permessage_deflate_params([], CB, CTO, SB, STO) -> + {CB, CTO, SB, STO}; +parse_response_permessage_deflate_params([{<<"client_max_window_bits">>, Max}|Tail], _, CTO, SB, STO) -> + case parse_max_window_bits(Max) of + error -> error; + CB -> parse_response_permessage_deflate_params(Tail, CB, CTO, SB, STO) + end; +parse_response_permessage_deflate_params([<<"client_no_context_takeover">>|Tail], CB, _, SB, STO) -> + parse_response_permessage_deflate_params(Tail, CB, no_takeover, SB, STO); +parse_response_permessage_deflate_params([{<<"server_max_window_bits">>, Max}|Tail], CB, CTO, _, STO) -> + case parse_max_window_bits(Max) of + error -> error; + SB -> parse_response_permessage_deflate_params(Tail, CB, CTO, SB, STO) + end; +parse_response_permessage_deflate_params([<<"server_no_context_takeover">>|Tail], CB, CTO, SB, _) -> + parse_response_permessage_deflate_params(Tail, CB, CTO, SB, no_takeover); +%% Error if unknown parameter; error if parameter with invalid or missing value. +parse_response_permessage_deflate_params(_, _, _, _, _) -> + error. %% @doc Parse and validate the Websocket frame header. %% @@ -47,8 +226,8 @@ %% that defines meanings for non-zero values. parse_header(<< _:1, Rsv:3, _/bits >>, Extensions, _) when Extensions =:= #{}, Rsv =/= 0 -> error; %% Last 2 RSV bits MUST be 0 if deflate-frame extension is used. -parse_header(<< _:2, 1:1, _/bits >>, #{deflate_frame := _}, _) -> error; -parse_header(<< _:3, 1:1, _/bits >>, #{deflate_frame := _}, _) -> error; +parse_header(<< _:2, 1:1, _/bits >>, #{deflate := _}, _) -> error; +parse_header(<< _:3, 1:1, _/bits >>, #{deflate := _}, _) -> error; %% Invalid opcode. Note that these opcodes may be used by extensions. parse_header(<< _:4, 3:4, _/bits >>, _, _) -> error; parse_header(<< _:4, 4:4, _/bits >>, _, _) -> error; @@ -65,13 +244,13 @@ parse_header(<< 0:1, _:3, Opcode:4, _/bits >>, _, _) when Opcode >= 8 -> error; %% A frame MUST NOT use the zero opcode unless fragmentation was initiated. parse_header(<< _:4, 0:4, _/bits >>, _, undefined) -> error; %% Non-control opcode when expecting control message or next fragment. -parse_header(<< _:4, 1:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 2:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 3:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 4:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 5:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 6:4, _/bits >>, _, {_, _}) -> error; -parse_header(<< _:4, 7:4, _/bits >>, _, {_, _}) -> error; +parse_header(<< _:4, 1:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 2:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 3:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 4:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 5:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 6:4, _/bits >>, _, {_, _, _}) -> error; +parse_header(<< _:4, 7:4, _/bits >>, _, {_, _, _}) -> error; %% Close control frame length MUST be 0 or >= 2. parse_header(<< _:4, 8:4, _:1, 1:7, _/bits >>, _, _) -> error; %% Close control frame with incomplete close code. Need more data. @@ -111,7 +290,7 @@ parse_header(Opcode, Fin, FragState, Rsv, Len, MaskKey, Rest) -> 0 -> fragment; 1 -> Type end, - {Type2, frag_state(Type, Fin, FragState), Rsv, Len, MaskKey, Rest}. + {Type2, frag_state(Type, Fin, Rsv, FragState), Rsv, Len, MaskKey, Rest}. opcode_to_frame_type(0) -> fragment; opcode_to_frame_type(1) -> text; @@ -120,25 +299,10 @@ opcode_to_frame_type(8) -> close; opcode_to_frame_type(9) -> ping; opcode_to_frame_type(10) -> pong. -frag_state(Type, 0, undefined) -> {nofin, Type}; -frag_state(fragment, 0, FragState = {nofin, _}) -> FragState; -frag_state(fragment, 1, {nofin, Type}) -> {fin, Type}; -frag_state(_, 1, FragState) -> FragState. - -%% @doc Parse and validate the close frame's close code. -%% -%% The close code is part of the payload and must therefore be unmasked. - --spec parse_close_code(binary(), mask_key()) -> {ok, close_code(), binary()} | error. -parse_close_code(<< MaskedCode:2/binary, Rest/bits >>, MaskKey) -> - << Code:16 >> = unmask(MaskedCode, MaskKey, 0), - if - Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006; - (Code > 1011) and (Code < 3000); Code > 4999 -> - error; - true -> - {ok, Code, Rest} - end. +frag_state(Type, 0, Rsv, undefined) -> {nofin, Type, Rsv}; +frag_state(fragment, 0, _, FragState = {nofin, _, _}) -> FragState; +frag_state(fragment, 1, _, {nofin, Type, Rsv}) -> {fin, Type, Rsv}; +frag_state(_, 1, _, FragState) -> FragState. %% @doc Parse and validate the frame's payload. %% @@ -148,10 +312,46 @@ parse_close_code(<< MaskedCode:2/binary, Rest/bits >>, MaskKey) -> -spec parse_payload(binary(), mask_key(), utf8_state(), non_neg_integer(), frame_type(), non_neg_integer(), frag_state(), extensions(), rsv()) -> {ok, binary(), utf8_state(), binary()} | {more, binary(), utf8_state()} | error. -parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState, #{deflate_frame := Inflate}, << 1:1, 0:2 >>) -> +%% Empty last frame of compressed message. +parse_payload(Data, _, Utf8State, _, _, 0, {fin, _, << 1:1, 0:2 >>}, + #{inflate := Inflate, inflate_takeover := TakeOver}, _) -> + zlib:inflate(Inflate, << 0, 0, 255, 255 >>), + case TakeOver of + no_takeover -> zlib:inflateReset(Inflate); + takeover -> ok + end, + {ok, <<>>, Utf8State, Data}; +%% Compressed fragmented frame. +parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState = {_, _, << 1:1, 0:2 >>}, + #{inflate := Inflate, inflate_takeover := TakeOver}, _) -> {Data2, Rest, Eof} = split_payload(Data, Len), - Payload = inflate_frame(unmask(Data2, MaskKey, ParsedLen), Inflate, FragState, Eof), + Payload = inflate_frame(unmask(Data2, MaskKey, ParsedLen), Inflate, TakeOver, FragState, Eof), validate_payload(Payload, Rest, Utf8State, ParsedLen, Type, FragState, Eof); +%% Compressed frame. +parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState, + #{inflate := Inflate, inflate_takeover := TakeOver}, << 1:1, 0:2 >>) when Type =:= text; Type =:= binary -> + {Data2, Rest, Eof} = split_payload(Data, Len), + Payload = inflate_frame(unmask(Data2, MaskKey, ParsedLen), Inflate, TakeOver, FragState, Eof), + validate_payload(Payload, Rest, Utf8State, ParsedLen, Type, FragState, Eof); +%% Empty frame. +parse_payload(Data, _, Utf8State = 0, 0, _, 0, _, _, _) -> + {ok, <<>>, Utf8State, Data}; +%% Start of close frame. +parse_payload(Data, MaskKey, Utf8State, 0, Type = close, Len, FragState, _, << 0:3 >>) -> + {<< MaskedCode:2/binary, Data2/bits >>, Rest, Eof} = split_payload(Data, Len), + << CloseCode:16 >> = unmask(MaskedCode, MaskKey, 0), + case validate_close_code(CloseCode) of + ok -> + Payload = unmask(Data2, MaskKey, 2), + case validate_payload(Payload, Rest, Utf8State, 2, Type, FragState, Eof) of + {ok, _, Utf8State2, _} -> {ok, CloseCode, Payload, Utf8State2, Rest}; + {more, _, Utf8State2} -> {more, CloseCode, Payload, Utf8State2}; + Error -> Error + end; + error -> + {error, badframe} + end; +%% Normal frame. parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState, _, << 0:3 >>) -> {Data2, Rest, Eof} = split_payload(Data, Len), Payload = unmask(Data2, MaskKey, ParsedLen), @@ -168,88 +368,125 @@ split_payload(Data, Len) -> {Data2, Rest, true} end. +validate_close_code(Code) -> + if + Code < 1000 -> error; + Code =:= 1004 -> error; + Code =:= 1005 -> error; + Code =:= 1006 -> error; + Code > 1011, Code < 3000 -> error; + Code > 4999 -> error; + true -> ok + end. + +unmask(Data, undefined, _) -> + Data; unmask(Data, MaskKey, 0) -> - do_unmask(Data, MaskKey, <<>>); + mask(Data, MaskKey, <<>>); %% We unmask on the fly so we need to continue from the right mask byte. unmask(Data, MaskKey, UnmaskedLen) -> Left = UnmaskedLen rem 4, Right = 4 - Left, MaskKey2 = (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)), - do_unmask(Data, MaskKey2, <<>>). + mask(Data, MaskKey2, <<>>). -do_unmask(<<>>, _, Unmasked) -> +mask(<<>>, _, Unmasked) -> Unmasked; -do_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) -> +mask(<< O:32, Rest/bits >>, MaskKey, Acc) -> T = O bxor MaskKey, - do_unmask(Rest, MaskKey, << Acc/binary, T:32 >>); -do_unmask(<< O:24 >>, MaskKey, Acc) -> + mask(Rest, MaskKey, << Acc/binary, T:32 >>); +mask(<< O:24 >>, MaskKey, Acc) -> << MaskKey2:24, _:8 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:24 >>; -do_unmask(<< O:16 >>, MaskKey, Acc) -> +mask(<< O:16 >>, MaskKey, Acc) -> << MaskKey2:16, _:16 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:16 >>; -do_unmask(<< O:8 >>, MaskKey, Acc) -> +mask(<< O:8 >>, MaskKey, Acc) -> << MaskKey2:8, _:24 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:8 >>. -%% @todo Try using iodata() and see if it improves anything. -inflate_frame(Data, Inflate, fin, true) -> - iolist_to_binary(zlib:inflate(Inflate, << Data/binary, 0, 0, 255, 255 >>)); -inflate_frame(Data, Inflate, _, _) -> +inflate_frame(Data, Inflate, TakeOver, FragState, true) + when FragState =:= undefined; element(1, FragState) =:= fin -> + Data2 = zlib:inflate(Inflate, << Data/binary, 0, 0, 255, 255 >>), + case TakeOver of + no_takeover -> zlib:inflateReset(Inflate); + takeover -> ok + end, + iolist_to_binary(Data2); +inflate_frame(Data, Inflate, _T, _F, _E) -> iolist_to_binary(zlib:inflate(Inflate, Data)). %% Text frames and close control frames MUST have a payload that is valid UTF-8. validate_payload(Payload, Rest, Utf8State, _, Type, _, Eof) when Type =:= text; Type =:= close -> - case validate_utf8(<< Utf8State/binary, Payload/binary >>) of - false -> error; - Utf8State when not Eof -> {more, Payload, Utf8State}; - <<>> when Eof -> {ok, Payload, <<>>, Rest}; - _ -> error + case validate_utf8(Payload, Utf8State) of + 1 -> {error, badencoding}; + Utf8State2 when not Eof -> {more, Payload, Utf8State2}; + 0 when Eof -> {ok, Payload, 0, Rest}; + _ -> {error, badencoding} end; -validate_payload(Payload, Rest, Utf8State, _, fragment, {Fin, text}, Eof) -> - case validate_utf8(<< Utf8State/binary, Payload/binary >>) of - false -> error; - <<>> when Eof -> {ok, Payload, <<>>, Rest}; +validate_payload(Payload, Rest, Utf8State, _, fragment, {Fin, text, _}, Eof) -> + case validate_utf8(Payload, Utf8State) of + 1 -> {error, badencoding}; + 0 when Eof -> {ok, Payload, 0, Rest}; Utf8State2 when Eof, Fin =:= nofin -> {ok, Payload, Utf8State2, Rest}; Utf8State2 when not Eof -> {more, Payload, Utf8State2}; - _ -> error + _ -> {error, badencoding} end; validate_payload(Payload, _, Utf8State, _, _, _, false) -> {more, Payload, Utf8State}; validate_payload(Payload, Rest, Utf8State, _, _, _, true) -> {ok, Payload, Utf8State, Rest}. -%% Returns <<>> if the argument is valid UTF-8, false if not, -%% or the incomplete part of the argument if we need more data. -validate_utf8(Valid = <<>>) -> - Valid; -validate_utf8(<< _/utf8, Rest/bits >>) -> - validate_utf8(Rest); -%% 2 bytes. Codepages C0 and C1 are invalid; fail early. -validate_utf8(<< 2#1100000:7, _/bits >>) -> - false; -validate_utf8(Incomplete = << 2#110:3, _:5 >>) -> - Incomplete; -%% 3 bytes. -validate_utf8(Incomplete = << 2#1110:4, _:4 >>) -> - Incomplete; -validate_utf8(Incomplete = << 2#1110:4, _:4, 2#10:2, _:6 >>) -> - Incomplete; -%% 4 bytes. Codepage F4 may have invalid values greater than 0x10FFFF. -validate_utf8(<< 2#11110100:8, 2#10:2, High:6, _/bits >>) when High >= 2#10000 -> - false; -validate_utf8(Incomplete = << 2#11110:5, _:3 >>) -> - Incomplete; -validate_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6 >>) -> - Incomplete; -validate_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6, 2#10:2, _:6 >>) -> - Incomplete; -%% Invalid. -validate_utf8(_) -> - false. +%% Based on the Flexible and Economical UTF-8 Decoder algorithm by +%% Bjoern Hoehrmann <[email protected]> (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). +%% +%% The original algorithm has been unrolled into all combinations of values for C and State +%% each with a clause. The common clauses were then grouped together. +%% +%% This function returns 0 on success, 1 on error, and 2..8 on incomplete data. +validate_utf8(<<>>, State) -> State; +validate_utf8(<< C, Rest/bits >>, 0) when C < 128 -> validate_utf8(Rest, 0); +validate_utf8(<< C, Rest/bits >>, 2) when C >= 128, C < 144 -> validate_utf8(Rest, 0); +validate_utf8(<< C, Rest/bits >>, 3) when C >= 128, C < 144 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 5) when C >= 128, C < 144 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 7) when C >= 128, C < 144 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 8) when C >= 128, C < 144 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 2) when C >= 144, C < 160 -> validate_utf8(Rest, 0); +validate_utf8(<< C, Rest/bits >>, 3) when C >= 144, C < 160 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 5) when C >= 144, C < 160 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 6) when C >= 144, C < 160 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 7) when C >= 144, C < 160 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 2) when C >= 160, C < 192 -> validate_utf8(Rest, 0); +validate_utf8(<< C, Rest/bits >>, 3) when C >= 160, C < 192 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 4) when C >= 160, C < 192 -> validate_utf8(Rest, 2); +validate_utf8(<< C, Rest/bits >>, 6) when C >= 160, C < 192 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 7) when C >= 160, C < 192 -> validate_utf8(Rest, 3); +validate_utf8(<< C, Rest/bits >>, 0) when C >= 194, C < 224 -> validate_utf8(Rest, 2); +validate_utf8(<< 224, Rest/bits >>, 0) -> validate_utf8(Rest, 4); +validate_utf8(<< C, Rest/bits >>, 0) when C >= 225, C < 237 -> validate_utf8(Rest, 3); +validate_utf8(<< 237, Rest/bits >>, 0) -> validate_utf8(Rest, 5); +validate_utf8(<< C, Rest/bits >>, 0) when C =:= 238; C =:= 239 -> validate_utf8(Rest, 3); +validate_utf8(<< 240, Rest/bits >>, 0) -> validate_utf8(Rest, 6); +validate_utf8(<< C, Rest/bits >>, 0) when C =:= 241; C =:= 242; C =:= 243 -> validate_utf8(Rest, 7); +validate_utf8(<< 244, Rest/bits >>, 0) -> validate_utf8(Rest, 8); +validate_utf8(_, _) -> 1. + +%% @doc Return a frame tuple from parsed state and data. + +-spec make_frame(frame_type(), binary(), close_code(), frag_state()) -> frame(). +%% Fragmented frame. +make_frame(fragment, Payload, _, {Fin, Type, _}) -> {fragment, Fin, Type, Payload}; +make_frame(text, Payload, _, _) -> {text, Payload}; +make_frame(binary, Payload, _, _) -> {binary, Payload}; +make_frame(close, <<>>, undefined, _) -> close; +make_frame(close, Payload, CloseCode, _) -> {close, CloseCode, Payload}; +make_frame(ping, <<>>, _, _) -> ping; +make_frame(ping, Payload, _, _) -> {ping, Payload}; +make_frame(pong, <<>>, _, _) -> pong; +make_frame(pong, Payload, _, _) -> {pong, Payload}. %% @doc Construct an unmasked Websocket frame. @@ -276,12 +513,12 @@ frame({pong, Payload}, _) -> true = Len =< 125, [<< 1:1, 0:3, 10:4, 0:1, Len:7 >>, Payload]; %% Data frames, deflate-frame extension. -frame({text, Payload}, #{deflate_frame := Deflate}) -> - Payload2 = deflate_frame(Payload, Deflate), +frame({text, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver}) -> + Payload2 = deflate_frame(Payload, Deflate, TakeOver), Len = payload_length(Payload2), [<< 1:1, 1:1, 0:2, 1:4, 0:1, Len/bits >>, Payload2]; -frame({binary, Payload}, #{deflate_frame := Deflate}) -> - Payload2 = deflate_frame(Payload, Deflate), +frame({binary, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver}) -> + Payload2 = deflate_frame(Payload, Deflate, TakeOver), Len = payload_length(Payload2), [<< 1:1, 1:1, 0:2, 2:4, 0:1, Len/bits >>, Payload2]; %% Data frames. @@ -292,6 +529,56 @@ frame({binary, Payload}, _) -> Len = payload_length(Payload), [<< 1:1, 0:3, 2:4, 0:1, Len/bits >>, Payload]. +%% @doc Construct a masked Websocket frame. +%% +%% We use a mask key of 0 if there is no payload for close, ping and pong frames. + +-spec masked_frame(frame(), extensions()) -> iodata(). +%% Control frames. Control packets must not be > 125 in length. +masked_frame(close, _) -> + << 1:1, 0:3, 8:4, 1:1, 0:39 >>; +masked_frame(ping, _) -> + << 1:1, 0:3, 9:4, 1:1, 0:39 >>; +masked_frame(pong, _) -> + << 1:1, 0:3, 10:4, 1:1, 0:39 >>; +masked_frame({close, Payload}, Extensions) -> + frame({close, 1000, Payload}, Extensions); +masked_frame({close, StatusCode, Payload}, _) -> + Len = 2 + iolist_size(Payload), + true = Len =< 125, + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + [<< 1:1, 0:3, 8:4, 1:1, Len:7 >>, MaskKeyBin, mask(iolist_to_binary([<< StatusCode:16 >>, Payload]), MaskKey, <<>>)]; +masked_frame({ping, Payload}, _) -> + Len = iolist_size(Payload), + true = Len =< 125, + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + [<< 1:1, 0:3, 9:4, 1:1, Len:7 >>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)]; +masked_frame({pong, Payload}, _) -> + Len = iolist_size(Payload), + true = Len =< 125, + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + [<< 1:1, 0:3, 10:4, 1:1, Len:7 >>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)]; +%% Data frames, deflate-frame extension. +masked_frame({text, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver}) -> + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + Payload2 = mask(deflate_frame(Payload, Deflate, TakeOver), MaskKey, <<>>), + Len = payload_length(Payload2), + [<< 1:1, 1:1, 0:2, 1:4, 1:1, Len/bits >>, MaskKeyBin, Payload2]; +masked_frame({binary, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver}) -> + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + Payload2 = mask(deflate_frame(Payload, Deflate, TakeOver), MaskKey, <<>>), + Len = payload_length(Payload2), + [<< 1:1, 1:1, 0:2, 2:4, 1:1, Len/bits >>, MaskKeyBin, Payload2]; +%% Data frames. +masked_frame({text, Payload}, _) -> + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + Len = payload_length(Payload), + [<< 1:1, 0:3, 1:4, 1:1, Len/bits >>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)]; +masked_frame({binary, Payload}, _) -> + MaskKeyBin = << MaskKey:32 >> = crypto:rand_bytes(4), + Len = payload_length(Payload), + [<< 1:1, 0:3, 2:4, 1:1, Len/bits >>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)]. + payload_length(Payload) -> case byte_size(Payload) of N when N =< 125 -> << N:7 >>; @@ -299,8 +586,12 @@ payload_length(Payload) -> N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> end. -deflate_frame(Payload, Deflate) -> +deflate_frame(Payload, Deflate, TakeOver) -> Deflated = iolist_to_binary(zlib:deflate(Deflate, Payload, sync)), + case TakeOver of + no_takeover -> zlib:deflateReset(Deflate); + takeover -> ok + end, Len = byte_size(Deflated) - 4, case Deflated of << Body:Len/binary, 0:8, 0:8, 255:8, 255:8 >> -> Body; diff --git a/src/cowlib.app.src b/src/cowlib.app.src index ec21de0..5d358c2 100644 --- a/src/cowlib.app.src +++ b/src/cowlib.app.src @@ -1,4 +1,4 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> +%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]> %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above @@ -14,7 +14,7 @@ {application, cowlib, [ {description, "Support library for manipulating Web protocols."}, - {vsn, "1.1.0"}, + {vsn, "1.2.0"}, {id, "git"}, {modules, []}, {registered, []}, diff --git a/test/eunit_SUITE.erl b/test/eunit_SUITE.erl deleted file mode 100644 index dddfdd3..0000000 --- a/test/eunit_SUITE.erl +++ /dev/null @@ -1,31 +0,0 @@ -%% Copyright (c) 2013-2014, Loïc Hoguin <[email protected]> -%% -%% Permission to use, copy, modify, and/or distribute this software for any -%% purpose with or without fee is hereby granted, provided that the above -%% copyright notice and this permission notice appear in all copies. -%% -%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - --module(eunit_SUITE). - --include_lib("common_test/include/ct.hrl"). - -%% ct. --export([all/0]). - -%% Tests. --export([eunit/1]). - -%% ct. - -all() -> - [eunit]. - -eunit(_) -> - ok = eunit:test({application, cowlib}). |