Created
October 21, 2009 08:12
-
-
Save anotherjesse/214961 to your computer and use it in GitHub Desktop.
s2 - stupid s3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| *.pyc | |
| *.beam |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/ | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a | |
| # copy of this software and associated documentation files (the | |
| # "Software"), to deal in the Software without restriction, including | |
| # without limitation the rights to use, copy, modify, merge, publish, dis- | |
| # tribute, sublicense, and/or sell copies of the Software, and to permit | |
| # persons to whom the Software is furnished to do so, subject to the fol- | |
| # lowing conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included | |
| # in all copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | |
| # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | |
| # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| # IN THE SOFTWARE. | |
| """ | |
| Some unit tests for the S3Connection | |
| """ | |
| import unittest | |
| import time | |
| import os | |
| import boto | |
| from boto.s3.connection import S3Connection | |
| from boto.exception import S3PermissionsError | |
| class S3ConnectionTest (unittest.TestCase): | |
| def test_1_basic(self): | |
| print '--- running S3Connection tests ---' | |
| c = S3Connection(aws_secret_access_key="foo", | |
| aws_access_key_id="bar", | |
| is_secure=False, | |
| debug=2, | |
| port=1234, | |
| host="localhost", | |
| calling_format=boto.s3.connection.OrdinaryCallingFormat()) | |
| # create a new, empty bucket | |
| bucket_name = 'test-%d' % int(time.time()) | |
| bucket = c.create_bucket(bucket_name) | |
| # now try a get_bucket call and see if it's really there | |
| bucket = c.get_bucket(bucket_name) | |
| # create a new key and store it's content from a string | |
| k = bucket.new_key() | |
| k.name = 'foobar' | |
| s1 = 'This is a test of file upload and download' | |
| s2 = 'This is a second string to test file upload and download' | |
| k.set_contents_from_string(s1) | |
| fp = open('foobar', 'wb') | |
| # now get the contents from s3 to a local file | |
| k.get_contents_to_file(fp) | |
| fp.close() | |
| fp = open('foobar') | |
| # check to make sure content read from s3 is identical to original | |
| assert s1 == fp.read(), 'corrupted file' | |
| fp.close() | |
| bucket.delete_key(k) | |
| # test a few variations on get_all_keys - first load some data | |
| # for the first one, let's override the content type | |
| phony_mimetype = 'application/x-boto-test' | |
| headers = {'Content-Type': phony_mimetype} | |
| k.name = 'foo/bar' | |
| k.set_contents_from_string(s1, headers) | |
| k.name = 'foo/bas' | |
| k.set_contents_from_filename('foobar') | |
| k.name = 'foo/bat' | |
| k.set_contents_from_string(s1) | |
| k.name = 'fie/bar' | |
| k.set_contents_from_string(s1) | |
| k.name = 'fie/bas' | |
| k.set_contents_from_string(s1) | |
| k.name = 'fie/bat' | |
| k.set_contents_from_string(s1) | |
| # try resetting the contents to another value | |
| md5 = k.md5 | |
| k.set_contents_from_string(s2) | |
| assert k.md5 != md5 | |
| os.unlink('foobar') | |
| all = bucket.get_all_keys() | |
| assert len(all) == 6 | |
| rs = bucket.get_all_keys(prefix='foo') | |
| assert len(rs) == 3 | |
| rs = bucket.get_all_keys(prefix='', delimiter='/') | |
| assert len(rs) == 2 | |
| rs = bucket.get_all_keys(maxkeys=5) | |
| assert len(rs) == 5 | |
| # test the lookup method | |
| k = bucket.lookup('foo/bar') | |
| assert isinstance(k, bucket.key_class) | |
| assert k.content_type == phony_mimetype | |
| k = bucket.lookup('notthere') | |
| assert k == None | |
| # try some metadata stuff | |
| k = bucket.new_key() | |
| k.name = 'has_metadata' | |
| mdkey1 = 'meta1' | |
| mdval1 = 'This is the first metadata value' | |
| k.set_metadata(mdkey1, mdval1) | |
| mdkey2 = 'meta2' | |
| mdval2 = 'This is the second metadata value' | |
| k.set_metadata(mdkey2, mdval2) | |
| k.set_contents_from_string(s1) | |
| k = bucket.lookup('has_metadata') | |
| assert k.get_metadata(mdkey1) == mdval1 | |
| assert k.get_metadata(mdkey2) == mdval2 | |
| k = bucket.new_key() | |
| k.name = 'has_metadata' | |
| k.get_contents_as_string() | |
| assert k.get_metadata(mdkey1) == mdval1 | |
| assert k.get_metadata(mdkey2) == mdval2 | |
| bucket.delete_key(k) | |
| # try a key with a funny character | |
| rs = bucket.get_all_keys() | |
| num_keys = len(rs) | |
| k = bucket.new_key() | |
| k.name = 'testnewline\n' | |
| k.set_contents_from_string('This is a test') | |
| rs = bucket.get_all_keys() | |
| assert len(rs) == num_keys + 1 | |
| bucket.delete_key(k) | |
| rs = bucket.get_all_keys() | |
| assert len(rs) == num_keys | |
| # try some acl stuff | |
| bucket.set_acl('public-read') | |
| policy = bucket.get_acl() | |
| assert len(policy.acl.grants) == 2 | |
| bucket.set_acl('private') | |
| policy = bucket.get_acl() | |
| assert len(policy.acl.grants) == 1 | |
| k = bucket.lookup('foo/bar') | |
| k.set_acl('public-read') | |
| policy = k.get_acl() | |
| assert len(policy.acl.grants) == 2 | |
| k.set_acl('private') | |
| policy = k.get_acl() | |
| assert len(policy.acl.grants) == 1 | |
| # try the convenience methods for grants | |
| bucket.add_user_grant('FULL_CONTROL', | |
| 'c1e724fbfa0979a4448393c59a8c055011f739b6d102fb37a65f26414653cd67') | |
| try: | |
| bucket.add_email_grant('foobar', 'foo@bar.com') | |
| except S3PermissionsError: | |
| pass | |
| # now delete all keys in bucket | |
| for k in all: | |
| bucket.delete_key(k) | |
| # now delete bucket | |
| c.delete_bucket(bucket) | |
| print '--- tests completed ---' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -module(bucket). | |
| -export([insert/2, | |
| fetch/1, | |
| first_run/0, | |
| start/0, | |
| stop/0]). | |
| -record(bucket, {index, owner}). | |
| start() -> | |
| ok = mnesia:start(), | |
| io:format("Waiting on mnesia tables..\n",[]), | |
| mnesia:wait_for_tables([bucket], 30000), | |
| mnesia:table_info(bucket, all), | |
| ok. | |
| stop() -> | |
| mnesia:stop(). | |
| first_run() -> | |
| mnesia:create_schema([node()]), | |
| ok = mnesia:start(), | |
| mnesia:create_table(bucket, | |
| [ {disc_copies, [node()] }, | |
| {attributes, | |
| record_info(fields,bucket)} ]). | |
| fetch(Id) -> | |
| Fun = | |
| fun() -> | |
| mnesia:read({bucket, Id}) | |
| end, | |
| case mnesia:transaction(Fun) of | |
| {atomic, []} -> | |
| not_found; | |
| {atomic, [Bucket]} -> | |
| Bucket#bucket.owner | |
| end. | |
| insert(Bucket, Owner) -> | |
| Fun = fun() -> | |
| mnesia:write( | |
| #bucket{ index = Bucket, | |
| owner = Owner } ) | |
| end, | |
| {atomic, Result} = mnesia:transaction(Fun), | |
| Result. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # init | |
| pwd=$(dirname $0) | |
| # compile in main src/ to ebin/ dir | |
| echo -n "compiling... " | |
| erlc -o ${pwd}/ebin/ ${pwd}/src/*.erl | |
| echo "ok." | |
| # copy .app file | |
| echo -n "copying... " | |
| cp ${pwd}/src/*.app ${pwd}/ebin/ | |
| echo "ok." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -module(meta). | |
| -export([insert/3, | |
| fetch/1, | |
| fetch/2, | |
| delete/2, | |
| first_run/0, | |
| start/0, | |
| stop/0]). | |
| -record(object, {index, | |
| bucket, | |
| key, | |
| headers}). | |
| start() -> | |
| ok = mnesia:start(), | |
| io:format("Waiting on mnesia tables..\n",[]), | |
| mnesia:wait_for_tables([object], 30000), | |
| mnesia:table_info(object, all), | |
| ok. | |
| stop() -> | |
| mnesia:stop(). | |
| first_run() -> | |
| mnesia:create_schema([node()]), | |
| ok = mnesia:start(), | |
| mnesia:create_table(object, | |
| [ {disc_copies, [node()] }, | |
| {attributes, | |
| record_info(fields,object)} ]). | |
| fetch(Bucket) -> | |
| Fun = | |
| fun() -> | |
| mnesia:match_object({object, '_', Bucket, '_', '_' } ) | |
| end, | |
| {atomic, Results} = mnesia:transaction( Fun), | |
| Results. | |
| fetch(Bucket, Key) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Fun = | |
| fun() -> | |
| mnesia:read({object, Id}) | |
| end, | |
| case mnesia:transaction(Fun) of | |
| {atomic, []} -> | |
| not_found; | |
| {atomic, [Object]} -> | |
| Object#object.headers | |
| end. | |
| insert(Bucket, Key, Headers) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Fun = fun() -> | |
| mnesia:write( | |
| #object{ index = Id, | |
| bucket = Bucket, | |
| key = Key, | |
| headers = Headers } ) | |
| end, | |
| {atomic, Result} = mnesia:transaction(Fun), | |
| Result. | |
| delete(Bucket, Key) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Delete=#object{ index = Id, _ = '_'}, | |
| Fun = fun() -> | |
| List = mnesia:match_object(Delete), | |
| lists:foreach(fun(X) -> | |
| mnesia:delete_object(X) | |
| end, List) | |
| end, | |
| mnesia:transaction(Fun). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {application, misultin, | |
| [ | |
| {description, "Lightweight HTTP Server Library"}, | |
| {vsn, '0.3'}, | |
| {modules, [misultin_socket, misultin_req, misultin]}, | |
| {registered, [misultin]}, | |
| {env, []}, | |
| {applications, [kernel, stdlib]} | |
| ]}. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| % ========================================================================================================== | |
| % MISULTIN - Main | |
| % | |
| % >-|-|-(°> | |
| % | |
| % Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde. | |
| % All rights reserved. | |
| % | |
| % Code portions from Sean Hinde have been originally taken under BSD license from Trapexit at the address: | |
| % <http://www.trapexit.org/A_fast_web_server_demonstrating_some_undocumented_Erlang_features> | |
| % | |
| % BSD License | |
| % | |
| % Redistribution and use in source and binary forms, with or without modification, are permitted provided | |
| % that the following conditions are met: | |
| % | |
| % * Redistributions of source code must retain the above copyright notice, this list of conditions and the | |
| % following disclaimer. | |
| % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and | |
| % the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote | |
| % products derived from this software without specific prior written permission. | |
| % | |
| % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED | |
| % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A | |
| % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | |
| % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
| % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| % POSSIBILITY OF SUCH DAMAGE. | |
| % ========================================================================================================== | |
| -module(misultin). | |
| -behaviour(gen_server). | |
| -vsn('0.3.1'). | |
| % gen_server callbacks | |
| -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). | |
| % API | |
| -export([start_link/1, stop/0, create_acceptor/0]). | |
| % macros | |
| -define(SERVER, ?MODULE). | |
| % records | |
| -record(state, { | |
| listen_socket, | |
| port, | |
| loop, | |
| acceptor, | |
| recv_timeout, | |
| stream_support | |
| }). | |
| % includes | |
| -include("../include/misultin.hrl"). | |
| % ============================ \/ API ====================================================================== | |
| % Function: {ok,Pid} | ignore | {error, Error} | |
| % Description: Starts the server. | |
| start_link(Options) when is_list(Options) -> | |
| gen_server:start_link({local, ?SERVER}, ?MODULE, [Options], []). | |
| %% Function: -> ok | |
| %% Description: Manually stops the server. | |
| stop() -> | |
| gen_server:cast(?SERVER, stop). | |
| %% Function: -> ok | |
| %% Description: Send message to cause a new acceptor to be created | |
| create_acceptor() -> | |
| gen_server:cast(?SERVER, create_acceptor). | |
| % ============================ /\ API ====================================================================== | |
| % ============================ \/ GEN_SERVER CALLBACKS ===================================================== | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: -> {ok, State} | {ok, State, Timeout} | ignore | {stop, Reason} | |
| % Description: Initiates the server. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| init([Options]) -> | |
| process_flag(trap_exit, true), | |
| ?DEBUG(info, "starting with Pid: ~p", [self()]), | |
| % test and get options | |
| OptionProps = [ | |
| {ip, {0, 0, 0, 0}, fun check_and_convert_string_to_ip/1, invalid_ip}, | |
| {port, 80, fun is_integer/1, port_not_integer}, | |
| {loop, {error, undefined_loop}, fun is_function/1, loop_not_function}, | |
| {backlog, 128, fun is_integer/1, backlog_not_integer}, | |
| {recv_timeout, 30*1000, fun is_integer/1, recv_timeout_not_integer}, | |
| {stream_support, true, fun is_boolean/1, invalid_stream_support_option} | |
| ], | |
| OptionsVerified = lists:foldl(fun(OptionName, Acc) -> [get_option(OptionName, Options)|Acc] end, [], OptionProps), | |
| case proplists:get_value(error, OptionsVerified) of | |
| undefined -> | |
| % get options | |
| Ip = proplists:get_value(ip, OptionsVerified), | |
| Port = proplists:get_value(port, OptionsVerified), | |
| Loop = proplists:get_value(loop, OptionsVerified), | |
| Backlog = proplists:get_value(backlog, OptionsVerified), | |
| RecvTimeout = proplists:get_value(recv_timeout, OptionsVerified), | |
| StreamSupport = proplists:get_value(stream_support, OptionsVerified), | |
| % ipv6 support | |
| ?DEBUG(debug, "ip address is: ~p", [Ip]), | |
| InetOpt = case Ip of | |
| {_, _, _, _} -> | |
| % IPv4 | |
| inet; | |
| {_, _, _, _, _, _, _, _} -> | |
| % IPv6 | |
| inet6 | |
| end, | |
| %% ok, no error found in options -> create listening socket. | |
| case gen_tcp:listen(Port, [binary, {packet, http}, InetOpt, {ip, Ip}, {reuseaddr, true}, {active, false}, {backlog, Backlog}]) of | |
| {ok, ListenSocket} -> | |
| % start listening | |
| ?DEBUG(debug, "starting listener loop", []), | |
| % create acceptor | |
| AcceptorPid = misultin_socket:start_link(ListenSocket, Port, Loop, | |
| RecvTimeout, StreamSupport), | |
| {ok, #state{listen_socket = ListenSocket, port = Port, | |
| loop = Loop, acceptor = AcceptorPid, | |
| recv_timeout = RecvTimeout, stream_support = StreamSupport}}; | |
| {error, Reason} -> | |
| ?DEBUG(error, "error starting: ~p", [Reason]), | |
| % error | |
| {stop, Reason} | |
| end; | |
| Reason -> | |
| % error found in options | |
| {stop, Reason} | |
| end. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: handle_call(Request, From, State) -> {reply, Reply, State} | {reply, Reply, State, Timeout} | | |
| % {noreply, State} | {noreply, State, Timeout} | | |
| % {stop, Reason, Reply, State} | {stop, Reason, State} | |
| % Description: Handling call messages. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % handle_call generic fallback | |
| handle_call(_Request, _From, State) -> | |
| {reply, undefined, State}. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: handle_cast(Msg, State) -> {noreply, State} | {noreply, State, Timeout} | {stop, Reason, State} | |
| % Description: Handling cast messages. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % manual shutdown | |
| handle_cast(stop, State) -> | |
| ?DEBUG(info, "manual shutdown..", []), | |
| {stop, normal, State}; | |
| % create | |
| handle_cast(create_acceptor, #state{listen_socket = ListenSocket, port = Port, loop = Loop, recv_timeout = RecvTimeout} = State) -> | |
| ?DEBUG(debug, "creating new acceptor process", []), | |
| AcceptorPid = misultin_socket:start_link(ListenSocket, Port, Loop, RecvTimeout), | |
| {noreply, State#state{acceptor = AcceptorPid}}; | |
| % handle_cast generic fallback (ignore) | |
| handle_cast(_Msg, State) -> | |
| ?DEBUG(warning, "received unknown cast message: ~p", [_Msg]), | |
| {noreply, State}. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: handle_info(Info, State) -> {noreply, State} | {noreply, State, Timeout} | {stop, Reason, State} | |
| % Description: Handling all non call/cast messages. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % The current acceptor has died, respawn | |
| handle_info({'EXIT', Pid, _Reason}, #state{listen_socket = ListenSocket, port = Port, loop = Loop, acceptor = Pid, recv_timeout = RecvTimeout} = State) -> | |
| ?DEBUG(warning, "acceptor has died with reason: ~p, respawning", [_Reason]), | |
| AcceptorPid = misultin_socket:start_link(ListenSocket, Port, Loop, RecvTimeout), | |
| {noreply, State#state{acceptor = AcceptorPid}}; | |
| % handle_info generic fallback (ignore) | |
| handle_info(_Info, State) -> | |
| ?DEBUG(warning, "received unknown info message: ~p", [_Info]), | |
| {noreply, State}. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: terminate(Reason, State) -> void() | |
| % Description: This function is called by a gen_server when it is about to terminate. When it returns, | |
| % the gen_server terminates with Reason. The return value is ignored. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| terminate(_Reason, #state{listen_socket = ListenSocket, acceptor = AcceptorPid}) -> | |
| ?DEBUG(info, "shutting down server with Pid ~p", [self()]), | |
| % kill acceptor - TODO: find a more gentle way to do so | |
| exit(AcceptorPid, kill), | |
| % stop gen_tcp | |
| gen_tcp:close(ListenSocket), | |
| terminated. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| % Function: code_change(OldVsn, State, Extra) -> {ok, NewState} | |
| % Description: Convert process state when code is changed. | |
| % ---------------------------------------------------------------------------------------------------------- | |
| code_change(_OldVsn, State, _Extra) -> | |
| {ok, State}. | |
| % ============================ /\ GEN_SERVER CALLBACKS ===================================================== | |
| % ============================ \/ INTERNAL FUNCTIONS ======================================================= | |
| % Function: -> false | IpTuple | |
| % Description: Checks and converts a string Ip to inet repr. | |
| check_and_convert_string_to_ip(Ip) -> | |
| case inet_parse:address(Ip) of | |
| {error, _Reason} -> | |
| false; | |
| {ok, IpTuple} -> | |
| IpTuple | |
| end. | |
| % Description: Validate and get misultin options. | |
| get_option({OptionName, DefaultValue, CheckAndConvertFun, FailTypeError}, Options) -> | |
| case proplists:get_value(OptionName, Options) of | |
| undefined -> | |
| case DefaultValue of | |
| {error, Reason} -> | |
| {error, Reason}; | |
| Value -> | |
| {OptionName, Value} | |
| end; | |
| Value -> | |
| case CheckAndConvertFun(Value) of | |
| false -> | |
| {error, {FailTypeError, Value}}; | |
| true -> | |
| {OptionName, Value}; | |
| OutValue -> | |
| {OptionName, OutValue} | |
| end | |
| end. | |
| % ============================ /\ INTERNAL FUNCTIONS ======================================================= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| % ========================================================================================================== | |
| % MISULTIN - Include file | |
| % | |
| % Copyright (C) 2009, Sean Hinde, Roberto Ostinelli <roberto@ostinelli.net> | |
| % All rights reserved. | |
| % | |
| % BSD License | |
| % | |
| % Redistribution and use in source and binary forms, with or without modification, are permitted provided | |
| % that the following conditions are met: | |
| % | |
| % * Redistributions of source code must retain the above copyright notice, this list of conditions and the | |
| % following disclaimer. | |
| % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and | |
| % the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote | |
| % products derived from this software without specific prior written permission. | |
| % | |
| % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED | |
| % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A | |
| % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | |
| % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
| % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| % POSSIBILITY OF SUCH DAMAGE. | |
| % ========================================================================================================== | |
| % define debug | |
| -ifdef(debug). | |
| -define(DEBUG(Level, Str, Args), | |
| % Level = error | warning | info | debug | |
| case Level of | |
| error -> | |
| erlang:apply(error_logger, error_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| warning -> | |
| case ?debug of | |
| debug -> | |
| erlang:apply(error_logger, info_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| info -> | |
| erlang:apply(error_logger, info_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| warning -> | |
| erlang:apply(error_logger, warning_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| _ -> | |
| ok | |
| end; | |
| info -> | |
| case ?debug of | |
| debug -> | |
| erlang:apply(error_logger, info_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| info -> | |
| erlang:apply(error_logger, info_msg, [lists:concat([" module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| _ -> | |
| ok | |
| end; | |
| debug -> | |
| case ?debug of | |
| debug -> | |
| erlang:apply(error_logger, info_msg, [lists:concat(["[DEBUG] module: ", ?MODULE, "~n line: ", ?LINE, "~n", Str, "~n"]), Args]); | |
| _ -> | |
| ok | |
| end; | |
| _ -> | |
| ok | |
| end | |
| ). | |
| -else. | |
| -define(DEBUG(Level, Str, Args), true). | |
| -endif. | |
| % Request | |
| -record(req, { | |
| peer_addr, % peer IP | undefined | |
| peer_port, % peer port | undefined | |
| connection = keep_alive, % keep_alive | close | |
| content_length, % Integer | |
| vsn, % {Maj,Min} | |
| method, % 'GET'|'POST' | |
| uri, % Truncated URI /index.html | |
| args = "", % Part of URI after ? | |
| headers, % [{Tag, Val}] | |
| body = <<>> % Content Body | |
| }). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| % ========================================================================================================== | |
| % MISULTIN - Request | |
| % | |
| % >-|-|-(°> | |
| % | |
| % Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>, | |
| % Bob Ippolito <bob@mochimedia.com> for Mochi Media, Inc. | |
| % All rights reserved. | |
| % | |
| % Code portions from Bob Ippolito have been originally taken under MIT license from MOCHIWEB: | |
| % <http://code.google.com/p/mochiweb/> | |
| % | |
| % BSD License | |
| % | |
| % Redistribution and use in source and binary forms, with or without modification, are permitted provided | |
| % that the following conditions are met: | |
| % | |
| % * Redistributions of source code must retain the above copyright notice, this list of conditions and the | |
| % following disclaimer. | |
| % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and | |
| % the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote | |
| % products derived from this software without specific prior written permission. | |
| % | |
| % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED | |
| % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A | |
| % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | |
| % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
| % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| % POSSIBILITY OF SUCH DAMAGE. | |
| % ========================================================================================================== | |
| -module(misultin_req, [Req, SocketPid]). | |
| -vsn('0.3.1'). | |
| % macros | |
| -define(PERCENT, 37). % $\% | |
| -define(FULLSTOP, 46). % $\. | |
| -define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse | |
| (C >= $a andalso C =< $f) orelse | |
| (C >= $A andalso C =< $F))). | |
| -define(FILE_READ_BUFFER, 64*1012). | |
| % API | |
| -export([raw/0]). | |
| -export([ok/1, ok/2, ok/3, respond/2, respond/3, respond/4, stream/1, stream/2, stream/3]). | |
| -export([get/1, parse_qs/0, parse_post/0, file/1, file/2, resource/1]). | |
| % includes | |
| -include("../include/misultin.hrl"). | |
| -include_lib("kernel/include/file.hrl"). | |
| % ============================ \/ API ====================================================================== | |
| % Description: Returns raw request content. | |
| raw() -> | |
| Req. | |
| % Description: Formats a 200 response. | |
| ok(Template) -> | |
| ok([], Template). | |
| ok(Headers, Template) -> | |
| respond(200, Headers, Template). | |
| ok(Headers, Template, Vars) -> | |
| respond(200, Headers, Template, Vars). | |
| % Description: Formats a response. | |
| respond(HttpCode, Template) -> | |
| respond(HttpCode, [], Template). | |
| respond(HttpCode, Headers, Template) -> | |
| {HttpCode, Headers, Template}. | |
| respond(HttpCode, Headers, Template, Vars) when is_list(Template) =:= true -> | |
| {HttpCode, Headers, io_lib:format(Template, Vars)}. | |
| % Description: Start stream. | |
| stream(close) -> | |
| catch SocketPid ! stream_close; | |
| stream(head) -> | |
| stream(head, 200, []); | |
| stream(Template) -> | |
| catch SocketPid ! {stream_data, Template}. | |
| stream(head, Headers) -> | |
| stream(head, 200, Headers); | |
| stream(Template, Vars) when is_list(Template) =:= true -> | |
| catch SocketPid ! {stream_data, io_lib:format(Template, Vars)}. | |
| stream(head, HttpCode, Headers) -> | |
| catch SocketPid ! {stream_head, HttpCode, Headers}. | |
| % Description: Sends a file to the browser. | |
| file(FilePath) -> | |
| file_send(FilePath, []). | |
| % Description: Sends a file for download. | |
| file(attachment, FilePath) -> | |
| % get filename | |
| FileName = filename:basename(FilePath), | |
| file_send(FilePath, [{'Content-Disposition', lists:flatten(io_lib:format("attachment; filename=~s", [FileName]))}]). | |
| % Description: Get request info. | |
| get(peer_addr) -> | |
| Req#req.peer_addr; | |
| get(peer_port) -> | |
| Req#req.peer_port; | |
| get(connection) -> | |
| Req#req.connection; | |
| get(content_length) -> | |
| Req#req.content_length; | |
| get(vsn) -> | |
| Req#req.vsn; | |
| get(method) -> | |
| Req#req.method; | |
| get(uri) -> | |
| Req#req.uri; | |
| get(args) -> | |
| Req#req.args; | |
| get(headers) -> | |
| Req#req.headers; | |
| get(body) -> | |
| Req#req.body. | |
| % Description: Parse QueryString | |
| parse_qs() -> | |
| parse_qs(Req#req.args). | |
| % Description: Parse Post | |
| parse_post() -> | |
| % get header confirmation | |
| case proplists:get_value('Content-Type', Req#req.headers) of | |
| undefined -> | |
| []; | |
| ContentType -> | |
| [Type|_CharSet] = string:tokens(ContentType, ";"), | |
| case Type of | |
| "application/x-www-form-urlencoded" -> | |
| parse_qs(Req#req.body); | |
| _Other -> | |
| [] | |
| end | |
| end. | |
| % Description: Sets resource elements for restful services. | |
| resource(Options) when is_list(Options) -> | |
| % clean uri | |
| {_UriType, RawUri} = Req#req.uri, | |
| Uri = lists:foldl(fun(Option, Acc) -> clean_uri(Option, Acc) end, RawUri, Options), | |
| % split | |
| string:tokens(Uri, "/"). | |
| % ============================ /\ API ====================================================================== | |
| % ============================ \/ INTERNAL FUNCTIONS ======================================================= | |
| % parse querystring & post | |
| parse_qs(Binary) when is_binary(Binary) -> | |
| parse_qs(binary_to_list(Binary)); | |
| parse_qs(String) -> | |
| parse_qs(String, []). | |
| parse_qs([], Acc) -> | |
| lists:reverse(Acc); | |
| parse_qs(String, Acc) -> | |
| {Key, Rest} = parse_qs_key(String), | |
| {Value, Rest1} = parse_qs_value(Rest), | |
| parse_qs(Rest1, [{Key, Value} | Acc]). | |
| parse_qs_key(String) -> | |
| parse_qs_key(String, []). | |
| parse_qs_key([], Acc) -> | |
| {qs_revdecode(Acc), ""}; | |
| parse_qs_key([$= | Rest], Acc) -> | |
| {qs_revdecode(Acc), Rest}; | |
| parse_qs_key(Rest=[$; | _], Acc) -> | |
| {qs_revdecode(Acc), Rest}; | |
| parse_qs_key(Rest=[$& | _], Acc) -> | |
| {qs_revdecode(Acc), Rest}; | |
| parse_qs_key([C | Rest], Acc) -> | |
| parse_qs_key(Rest, [C | Acc]). | |
| parse_qs_value(String) -> | |
| parse_qs_value(String, []). | |
| parse_qs_value([], Acc) -> | |
| {qs_revdecode(Acc), ""}; | |
| parse_qs_value([$; | Rest], Acc) -> | |
| {qs_revdecode(Acc), Rest}; | |
| parse_qs_value([$& | Rest], Acc) -> | |
| {qs_revdecode(Acc), Rest}; | |
| parse_qs_value([C | Rest], Acc) -> | |
| parse_qs_value(Rest, [C | Acc]). | |
| % revdecode | |
| qs_revdecode(S) -> | |
| qs_revdecode(S, []). | |
| qs_revdecode([], Acc) -> | |
| Acc; | |
| qs_revdecode([$+ | Rest], Acc) -> | |
| qs_revdecode(Rest, [$\s | Acc]); | |
| qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) -> | |
| qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]); | |
| qs_revdecode([C | Rest], Acc) -> | |
| qs_revdecode(Rest, [C | Acc]). | |
| % unexdigit | |
| unhexdigit(C) when C >= $0, C =< $9 -> C - $0; | |
| unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10; | |
| unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10. | |
| % unquote | |
| unquote(Binary) when is_binary(Binary) -> | |
| unquote(binary_to_list(Binary)); | |
| unquote(String) -> | |
| qs_revdecode(lists:reverse(String)). | |
| % get content type | |
| get_content_type(FileName) -> | |
| case filename:extension(FileName) of | |
| % most common first | |
| ".doc" -> "application/msword"; | |
| ".exe" -> "application/octet-stream"; | |
| ".pdf" -> "application/pdf"; | |
| ".rtf" -> "application/rtf"; | |
| ".ppt" -> "application/vnd.ms-powerpoint"; | |
| ".tgz" -> "application/x-compressed"; | |
| ".tar" -> "application/x-tar"; | |
| ".zip" -> "application/zip"; | |
| ".mp3" -> "audio/mpeg"; | |
| ".wav" -> "audio/x-wav"; | |
| ".bmp" -> "image/bmp"; | |
| ".ram" -> "audio/x-pn-realaudio"; | |
| ".gif" -> "image/gif"; | |
| ".jpe" -> "image/jpeg"; | |
| ".jpeg" -> "image/jpeg"; | |
| ".jpg" -> "image/jpeg"; | |
| ".tif" -> "image/tiff"; | |
| ".tiff" -> "image/tiff"; | |
| ".htm" -> "text/html"; | |
| ".html" -> "text/html"; | |
| ".txt" -> "text/plain"; | |
| ".mp2" -> "video/mpeg"; | |
| ".mpa" -> "video/mpeg"; | |
| ".mpe" -> "video/mpeg"; | |
| ".mpeg" -> "video/mpeg"; | |
| ".mpg" -> "video/mpeg"; | |
| ".mov" -> "video/quicktime"; | |
| ".avi" -> "video/x-msvideo"; | |
| % less common last | |
| ".evy" -> "application/envoy"; | |
| ".fif" -> "application/fractals"; | |
| ".spl" -> "application/futuresplash"; | |
| ".hta" -> "application/hta"; | |
| ".acx" -> "application/internet-property-stream"; | |
| ".hqx" -> "application/mac-binhex40"; | |
| ".dot" -> "application/msword"; | |
| ".bin" -> "application/octet-stream"; | |
| ".class" -> "application/octet-stream"; | |
| ".dms" -> "application/octet-stream"; | |
| ".lha" -> "application/octet-stream"; | |
| ".lzh" -> "application/octet-stream"; | |
| ".oda" -> "application/oda"; | |
| ".axs" -> "application/olescript"; | |
| ".prf" -> "application/pics-rules"; | |
| ".p10" -> "application/pkcs10"; | |
| ".crl" -> "application/pkix-crl"; | |
| ".ai" -> "application/postscript"; | |
| ".eps" -> "application/postscript"; | |
| ".ps" -> "application/postscript"; | |
| ".setpay" -> "application/set-payment-initiation"; | |
| ".setreg" -> "application/set-registration-initiation"; | |
| ".xla" -> "application/vnd.ms-excel"; | |
| ".xlc" -> "application/vnd.ms-excel"; | |
| ".xlm" -> "application/vnd.ms-excel"; | |
| ".xls" -> "application/vnd.ms-excel"; | |
| ".xlt" -> "application/vnd.ms-excel"; | |
| ".xlw" -> "application/vnd.ms-excel"; | |
| ".msg" -> "application/vnd.ms-outlook"; | |
| ".sst" -> "application/vnd.ms-pkicertstore"; | |
| ".cat" -> "application/vnd.ms-pkiseccat"; | |
| ".stl" -> "application/vnd.ms-pkistl"; | |
| ".pot" -> "application/vnd.ms-powerpoint"; | |
| ".pps" -> "application/vnd.ms-powerpoint"; | |
| ".mpp" -> "application/vnd.ms-project"; | |
| ".wcm" -> "application/vnd.ms-works"; | |
| ".wdb" -> "application/vnd.ms-works"; | |
| ".wks" -> "application/vnd.ms-works"; | |
| ".wps" -> "application/vnd.ms-works"; | |
| ".hlp" -> "application/winhlp"; | |
| ".bcpio" -> "application/x-bcpio"; | |
| ".cdf" -> "application/x-cdf"; | |
| ".z" -> "application/x-compress"; | |
| ".cpio" -> "application/x-cpio"; | |
| ".csh" -> "application/x-csh"; | |
| ".dcr" -> "application/x-director"; | |
| ".dir" -> "application/x-director"; | |
| ".dxr" -> "application/x-director"; | |
| ".dvi" -> "application/x-dvi"; | |
| ".gtar" -> "application/x-gtar"; | |
| ".gz" -> "application/x-gzip"; | |
| ".hdf" -> "application/x-hdf"; | |
| ".ins" -> "application/x-internet-signup"; | |
| ".isp" -> "application/x-internet-signup"; | |
| ".iii" -> "application/x-iphone"; | |
| ".js" -> "application/x-javascript"; | |
| ".latex" -> "application/x-latex"; | |
| ".mdb" -> "application/x-msaccess"; | |
| ".crd" -> "application/x-mscardfile"; | |
| ".clp" -> "application/x-msclip"; | |
| ".dll" -> "application/x-msdownload"; | |
| ".m13" -> "application/x-msmediaview"; | |
| ".m14" -> "application/x-msmediaview"; | |
| ".mvb" -> "application/x-msmediaview"; | |
| ".wmf" -> "application/x-msmetafile"; | |
| ".mny" -> "application/x-msmoney"; | |
| ".pub" -> "application/x-mspublisher"; | |
| ".scd" -> "application/x-msschedule"; | |
| ".trm" -> "application/x-msterminal"; | |
| ".wri" -> "application/x-mswrite"; | |
| ".nc" -> "application/x-netcdf"; | |
| ".pma" -> "application/x-perfmon"; | |
| ".pmc" -> "application/x-perfmon"; | |
| ".pml" -> "application/x-perfmon"; | |
| ".pmr" -> "application/x-perfmon"; | |
| ".pmw" -> "application/x-perfmon"; | |
| ".p12" -> "application/x-pkcs12"; | |
| ".pfx" -> "application/x-pkcs12"; | |
| ".p7b" -> "application/x-pkcs7-certificates"; | |
| ".spc" -> "application/x-pkcs7-certificates"; | |
| ".p7r" -> "application/x-pkcs7-certreqresp"; | |
| ".p7c" -> "application/x-pkcs7-mime"; | |
| ".p7m" -> "application/x-pkcs7-mime"; | |
| ".p7s" -> "application/x-pkcs7-signature"; | |
| ".sh" -> "application/x-sh"; | |
| ".shar" -> "application/x-shar"; | |
| ".swf" -> "application/x-shockwave-flash"; | |
| ".sit" -> "application/x-stuffit"; | |
| ".sv4cpio" -> "application/x-sv4cpio"; | |
| ".sv4crc" -> "application/x-sv4crc"; | |
| ".tcl" -> "application/x-tcl"; | |
| ".tex" -> "application/x-tex"; | |
| ".texi" -> "application/x-texinfo"; | |
| ".texinfo" -> "application/x-texinfo"; | |
| ".roff" -> "application/x-troff"; | |
| ".t" -> "application/x-troff"; | |
| ".tr" -> "application/x-troff"; | |
| ".man" -> "application/x-troff-man"; | |
| ".me" -> "application/x-troff-me"; | |
| ".ms" -> "application/x-troff-ms"; | |
| ".ustar" -> "application/x-ustar"; | |
| ".src" -> "application/x-wais-source"; | |
| ".cer" -> "application/x-x509-ca-cert"; | |
| ".crt" -> "application/x-x509-ca-cert"; | |
| ".der" -> "application/x-x509-ca-cert"; | |
| ".pko" -> "application/ynd.ms-pkipko"; | |
| ".au" -> "audio/basic"; | |
| ".snd" -> "audio/basic"; | |
| ".mid" -> "audio/mid"; | |
| ".rmi" -> "audio/mid"; | |
| ".aif" -> "audio/x-aiff"; | |
| ".aifc" -> "audio/x-aiff"; | |
| ".aiff" -> "audio/x-aiff"; | |
| ".m3u" -> "audio/x-mpegurl"; | |
| ".ra" -> "audio/x-pn-realaudio"; | |
| ".cod" -> "image/cis-cod"; | |
| ".ief" -> "image/ief"; | |
| ".jfif" -> "image/pipeg"; | |
| ".svg" -> "image/svg+xml"; | |
| ".ras" -> "image/x-cmu-raster"; | |
| ".cmx" -> "image/x-cmx"; | |
| ".ico" -> "image/x-icon"; | |
| ".pnm" -> "image/x-portable-anymap"; | |
| ".pbm" -> "image/x-portable-bitmap"; | |
| ".pgm" -> "image/x-portable-graymap"; | |
| ".ppm" -> "image/x-portable-pixmap"; | |
| ".rgb" -> "image/x-rgb"; | |
| ".xbm" -> "image/x-xbitmap"; | |
| ".xpm" -> "image/x-xpixmap"; | |
| ".xwd" -> "image/x-xwindowdump"; | |
| ".mht" -> "message/rfc822"; | |
| ".mhtml" -> "message/rfc822"; | |
| ".nws" -> "message/rfc822"; | |
| ".css" -> "text/css"; | |
| ".323" -> "text/h323"; | |
| ".stm" -> "text/html"; | |
| ".uls" -> "text/iuls"; | |
| ".bas" -> "text/plain"; | |
| ".c" -> "text/plain"; | |
| ".h" -> "text/plain"; | |
| ".rtx" -> "text/richtext"; | |
| ".sct" -> "text/scriptlet"; | |
| ".tsv" -> "text/tab-separated-values"; | |
| ".htt" -> "text/webviewhtml"; | |
| ".htc" -> "text/x-component"; | |
| ".etx" -> "text/x-setext"; | |
| ".vcf" -> "text/x-vcard"; | |
| ".mpv2" -> "video/mpeg"; | |
| ".qt" -> "video/quicktime"; | |
| ".lsf" -> "video/x-la-asf"; | |
| ".lsx" -> "video/x-la-asf"; | |
| ".asf" -> "video/x-ms-asf"; | |
| ".asr" -> "video/x-ms-asf"; | |
| ".asx" -> "video/x-ms-asf"; | |
| ".movie" -> "video/x-sgi-movie"; | |
| ".flr" -> "x-world/x-vrml"; | |
| ".vrml" -> "x-world/x-vrml"; | |
| ".wrl" -> "x-world/x-vrml"; | |
| ".wrz" -> "x-world/x-vrml"; | |
| ".xaf" -> "x-world/x-vrml"; | |
| ".xof" -> "x-world/x-vrml"; | |
| _ -> "application/octet-stream" | |
| end. | |
| % Description: Clean URI. | |
| clean_uri(lowercase, Uri) -> | |
| string:to_lower(Uri); | |
| clean_uri(urldecode, Uri) -> | |
| unquote(Uri); | |
| % ignore unexisting option | |
| clean_uri(_Unavailable, Uri) -> | |
| Uri. | |
| % sending of a file | |
| file_send(FilePath, Headers) -> | |
| % get file size | |
| case file:read_file_info(FilePath) of | |
| {ok, FileInfo} -> | |
| % get filesize | |
| FileSize = FileInfo#file_info.size, | |
| % send headers | |
| HeadersFull = [{'Content-Type', get_content_type(FilePath)}, {'Content-Size', FileSize} | Headers], | |
| stream(head, HeadersFull), | |
| % do the gradual sending | |
| case file_open_and_send(FilePath) of | |
| {error, _Reason} -> | |
| {raw, misultin_utility:get_http_status_code(500)}; | |
| ok -> | |
| % sending successful | |
| ok | |
| end; | |
| {error, _Reason} -> | |
| {raw, misultin_utility:get_http_status_code(500)} | |
| end. | |
| file_open_and_send(FilePath) -> | |
| case file:open(FilePath, [read, binary]) of | |
| {error, Reason} -> | |
| {error, Reason}; | |
| {ok, IoDevice} -> | |
| % read portions | |
| case file_read_and_send(IoDevice, 0) of | |
| {error, Reason} -> | |
| file:close(IoDevice), | |
| {error, Reason}; | |
| ok -> | |
| file:close(IoDevice), | |
| ok | |
| end | |
| end. | |
| file_read_and_send(IoDevice, Position) -> | |
| % read buffer | |
| case file:pread(IoDevice, Position, ?FILE_READ_BUFFER) of | |
| {ok, Data} -> | |
| % file read, send | |
| stream(Data), | |
| % loop | |
| file_read_and_send(IoDevice, Position + ?FILE_READ_BUFFER); | |
| eof -> | |
| % finished, close | |
| stream(close), | |
| ok; | |
| {error, Reason} -> | |
| {error, Reason} | |
| end. | |
| % ============================ /\ INTERNAL FUNCTIONS ======================================================= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| % ========================================================================================================== | |
| % MISULTIN - Socket | |
| % | |
| % >-|-|-(°> | |
| % | |
| % Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde. | |
| % All rights reserved. | |
| % | |
| % Code portions from Sean Hinde have been originally taken under BSD license from Trapexit at the address: | |
| % <http://www.trapexit.org/A_fast_web_server_demonstrating_some_undocumented_Erlang_features> | |
| % | |
| % BSD License | |
| % | |
| % Redistribution and use in source and binary forms, with or without modification, are permitted provided | |
| % that the following conditions are met: | |
| % | |
| % * Redistributions of source code must retain the above copyright notice, this list of conditions and the | |
| % following disclaimer. | |
| % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and | |
| % the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote | |
| % products derived from this software without specific prior written permission. | |
| % | |
| % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED | |
| % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A | |
| % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | |
| % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
| % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| % POSSIBILITY OF SUCH DAMAGE. | |
| % ========================================================================================================== | |
| -module(misultin_socket). | |
| -vsn('0.3.1'). | |
| % API | |
| -export([start_link/5]). | |
| % callbacks | |
| -export([listener/5]). | |
| % internale | |
| -export([socket_loop/1]). | |
| % macros | |
| -define(MAX_HEADERS_COUNT, 100). | |
| % records | |
| -record(c, { | |
| sock, | |
| port, | |
| loop, | |
| recv_timeout, | |
| stream_support | |
| }). | |
| % includes | |
| -include("../include/misultin.hrl"). | |
| % ============================ \/ API ====================================================================== | |
| % Function: {ok,Pid} | ignore | {error, Error} | |
| % Description: Starts the socket. | |
| start_link(ListenSocket, ListenPort, Loop, RecvTimeout, StreamSupport) -> | |
| proc_lib:spawn_link(?MODULE, listener, [ListenSocket, ListenPort, Loop, RecvTimeout, StreamSupport]). | |
| % Function: {ok,Pid} | ignore | {error, Error} | |
| % Description: Starts the socket. | |
| listener(ListenSocket, ListenPort, Loop, RecvTimeout, StreamSupport) -> | |
| case catch gen_tcp:accept(ListenSocket) of | |
| {ok, Sock} -> | |
| ?DEBUG(debug, "accepted an incoming TCP connection, spawning controlling process", []), | |
| Pid = spawn(fun () -> | |
| receive | |
| set -> | |
| ?DEBUG(debug, "activated controlling process", []), | |
| ok | |
| after 60000 -> | |
| exit({error, controlling_failed}) | |
| end, | |
| % build connection record | |
| {ok, {Addr, Port}} = inet:peername(Sock), | |
| C = #c{sock = Sock, port = ListenPort, loop = Loop, recv_timeout = RecvTimeout, stream_support = StreamSupport}, | |
| % jump to state 'request' | |
| ?DEBUG(debug, "jump to state request", []), | |
| request(C, #req{peer_addr = Addr, peer_port = Port}) | |
| end), | |
| % set controlling process | |
| gen_tcp:controlling_process(Sock, Pid), | |
| Pid ! set, | |
| % get back to accept loop | |
| listener(ListenSocket, ListenPort, Loop, RecvTimeout, StreamSupport); | |
| _Else -> | |
| ?DEBUG(error, "accept failed error: ~p", [_Else]), | |
| exit({error, accept_failed}) | |
| end. | |
| % ============================ /\ API ====================================================================== | |
| % ============================ \/ INTERNAL FUNCTIONS ======================================================= | |
| % REQUEST: wait for a HTTP Request line. Transition to state headers if one is received. | |
| request(#c{sock = Sock, recv_timeout = RecvTimeout} = C, Req) -> | |
| inet:setopts(Sock, [{active, once}]), | |
| receive | |
| {http, Sock, {http_request, Method, Path, Version}} -> | |
| ?DEBUG(debug, "received full headers of a new HTTP packet", []), | |
| headers(C, Req#req{vsn = Version, method = Method, uri = Path, connection = default_connection(Version)}, []); | |
| {http, Sock, {http_error, "\r\n"}} -> | |
| request(C, Req); | |
| {http, Sock, {http_error, "\n"}} -> | |
| request(C, Req); | |
| {http, Sock, _Other} -> | |
| ?DEBUG(debug, "tcp error on incoming request: ~p, send bad request error back", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(400)) | |
| after RecvTimeout -> | |
| ?DEBUG(debug, "normal receive timeout, exit", []), | |
| exit(normal) | |
| end. | |
| % HEADERS: collect HTTP headers. After the end of header marker transition to body state. | |
| headers(C, Req, H) -> | |
| headers(C, Req, H, 0). | |
| headers(#c{sock = Sock}, _Req, _H, ?MAX_HEADERS_COUNT) -> | |
| ?DEBUG(debug, "too many headers sent, bad request",[]), | |
| send(Sock, misultin_utility:get_http_status_code(400)); | |
| headers(#c{sock = Sock, recv_timeout = RecvTimeout} = C, Req, H, HeaderCount) -> | |
| inet:setopts(Sock, [{active, once}]), | |
| receive | |
| {http, Sock, {http_header, _, 'Content-Length', _, Val}} -> | |
| headers(C, Req#req{content_length = Val}, [{'Content-Length', Val}|H], HeaderCount + 1); | |
| {http, Sock, {http_header, _, 'Connection', _, Val}} -> | |
| KeepAlive = keep_alive(Req#req.vsn, Val), | |
| headers(C, Req#req{connection = KeepAlive}, [{'Connection', Val}|H], HeaderCount + 1); | |
| {http, Sock, {http_header, _, Header, _, Val}} -> | |
| headers(C, Req, [{Header, Val}|H], HeaderCount + 1); | |
| {http, Sock, {http_error, "\r\n"}} -> | |
| headers(C, Req, H, HeaderCount); | |
| {http, Sock, {http_error, "\n"}} -> | |
| headers(C, Req, H, HeaderCount); | |
| {http, Sock, http_eoh} -> | |
| body(C, Req#req{headers = lists:reverse(H)}); | |
| {http, Sock, _Other} -> | |
| ?DEBUG(debug, "tcp error treating headers: ~p, send bad request error back", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(400)) | |
| after RecvTimeout -> | |
| ?DEBUG(debug, "headers timeout, sending request timeout error", []), | |
| send(Sock, misultin_utility:get_http_status_code(408)) | |
| end. | |
| % default connection | |
| default_connection({1,1}) -> keep_alive; | |
| default_connection(_) -> close. | |
| % Shall we keep the connection alive? Default case for HTTP/1.1 is yes, default for HTTP/1.0 is no. | |
| keep_alive({1,1}, "close") -> close; | |
| keep_alive({1,1}, "Close") -> close; | |
| % string:to_upper is used only as last resort. | |
| keep_alive({1,1}, Head) -> | |
| case string:to_upper(Head) of | |
| "CLOSE" -> close; | |
| _ -> keep_alive | |
| end; | |
| keep_alive({1,0}, "Keep-Alive") -> keep_alive; | |
| keep_alive({1,0}, Head) -> | |
| case string:to_upper(Head) of | |
| "KEEP-ALIVE" -> keep_alive; | |
| _ -> close | |
| end; | |
| keep_alive({0,9}, _) -> close; | |
| keep_alive(_Vsn, _KA) -> close. | |
| % BODY: collect the body of the HTTP request if there is one, and lookup and call the implementation callback. | |
| % Depending on whether the request is persistent transition back to state request to await the next request or exit. | |
| body(#c{sock = Sock, recv_timeout = RecvTimeout} = C, Req) -> | |
| case Req#req.method of | |
| 'GET' -> | |
| Close = handle_get(C, Req), | |
| case Close of | |
| close -> | |
| gen_tcp:close(Sock); | |
| keep_alive -> | |
| request(C, #req{peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port}) | |
| end; | |
| 'DELETE' -> | |
| Close = handle_get(C, Req), | |
| case Close of | |
| close -> | |
| gen_tcp:close(Sock); | |
| keep_alive -> | |
| request(C, #req{peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port}) | |
| end; | |
| 'PUT' -> | |
| io:format("Do the PUT~n"), | |
| case catch list_to_integer(Req#req.content_length) of | |
| {'EXIT', _} -> | |
| io:format("PUT ERROR~n"), | |
| %% TODO: provide a fallback when content length is not or wrongly specified | |
| ?DEBUG(debug, "specified content length is not a valid integer number: ~p", [Req#req.content_length]), | |
| send(Sock, misultin_utility:get_http_status_code(411)), | |
| exit(normal); | |
| 0 -> | |
| io:format("Handling PUT as a GET~n"), | |
| Close = handle_get(C, Req), | |
| case Close of | |
| close -> | |
| gen_tcp:close(Sock); | |
| keep_alive -> | |
| request(C, #req{peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port}) | |
| end; | |
| Len -> | |
| io:format("Handling PUT as a POST~n"), | |
| inet:setopts(Sock, [{packet, raw}, {active, false}]), | |
| case gen_tcp:recv(Sock, Len, RecvTimeout) of | |
| {ok, Bin} -> | |
| Close = handle_post(C, Req#req{body = Bin}), | |
| case Close of | |
| close -> | |
| gen_tcp:close(Sock); | |
| keep_alive -> | |
| inet:setopts(Sock, [{packet, http}]), | |
| request(C, #req{peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port}) | |
| end; | |
| {error, timeout} -> | |
| ?DEBUG(debug, "request timeout, sending error", []), | |
| send(Sock, misultin_utility:get_http_status_code(408)); | |
| _Other -> | |
| ?DEBUG(debug, "tcp error treating post data: ~p, send bad request error back", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(400)) | |
| end; | |
| _Other -> | |
| io:format("method not implemented ~p", [_Other]), | |
| ?DEBUG(debug, "method not implemented: ~p", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(501)), | |
| exit(normal) | |
| end; | |
| 'POST' -> | |
| case catch list_to_integer(Req#req.content_length) of | |
| {'EXIT', _} -> | |
| %% TODO: provide a fallback when content length is not or wrongly specified | |
| ?DEBUG(debug, "specified content length is not a valid integer number: ~p", [Req#req.content_length]), | |
| send(Sock, misultin_utility:get_http_status_code(411)), | |
| exit(normal); | |
| Len -> | |
| inet:setopts(Sock, [{packet, raw}, {active, false}]), | |
| case gen_tcp:recv(Sock, Len, RecvTimeout) of | |
| {ok, Bin} -> | |
| Close = handle_post(C, Req#req{body = Bin}), | |
| case Close of | |
| close -> | |
| gen_tcp:close(Sock); | |
| keep_alive -> | |
| inet:setopts(Sock, [{packet, http}]), | |
| request(C, #req{peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port}) | |
| end; | |
| {error, timeout} -> | |
| ?DEBUG(debug, "request timeout, sending error", []), | |
| send(Sock, misultin_utility:get_http_status_code(408)); | |
| _Other -> | |
| ?DEBUG(debug, "tcp error treating post data: ~p, send bad request error back", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(400)) | |
| end | |
| end; | |
| _Other -> | |
| ?DEBUG(debug, "method not implemented: ~p", [_Other]), | |
| send(Sock, misultin_utility:get_http_status_code(501)), | |
| exit(normal) | |
| end. | |
| % handle a get request | |
| handle_get(C, #req{connection = Conn} = Req) -> | |
| case Req#req.uri of | |
| {abs_path, Path} -> | |
| {F, Args} = split_at_q_mark(Path, []), | |
| call_mfa(C, Req#req{args = Args, uri = {abs_path, F}}), | |
| Conn; | |
| {absoluteURI, http, _Host, _, Path} -> | |
| {F, Args} = split_at_q_mark(Path, []), | |
| call_mfa(C, Req#req{args = Args, uri = {absoluteURI, F}}), | |
| Conn; | |
| {absoluteURI, _Other_method, _Host, _, _Path} -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(501)), | |
| close; | |
| {scheme, _Scheme, _RequestString} -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(510)), | |
| close; | |
| _ -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(403)), | |
| close | |
| end. | |
| % handle a post request | |
| handle_post(C, #req{connection = Conn} = Req) -> | |
| case Req#req.uri of | |
| {abs_path, _Path} -> | |
| call_mfa(C, Req), | |
| Conn; | |
| {absoluteURI, http, _Host, _, _Path} -> | |
| call_mfa(C, Req), | |
| Conn; | |
| {absoluteURI, _Other_method, _Host, _, _Path} -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(501)), | |
| close; | |
| {scheme, _Scheme, _RequestString} -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(501)), | |
| close; | |
| _ -> | |
| send(C#c.sock, misultin_utility:get_http_status_code(403)), | |
| close | |
| end. | |
| % Description: Main dispatcher | |
| call_mfa(#c{sock = Sock, loop = Loop, stream_support = StreamSupport} = C, Request) -> | |
| % spawn listening process for Request messages [only used to support stream requests] | |
| case StreamSupport of | |
| true -> | |
| SocketPid = spawn(?MODULE, socket_loop, [C]); | |
| false -> | |
| SocketPid = no_stream_support_proc | |
| end, | |
| % create request | |
| Req = misultin_req:new(Request, SocketPid), | |
| % call loop | |
| case catch Loop(Req) of | |
| {'EXIT', _Reason} -> | |
| ?DEBUG(error, "worker crash: ~p", [_Reason]), | |
| % kill listening socket | |
| catch SocketPid ! shutdown, | |
| % send response | |
| send(Sock, misultin_utility:get_http_status_code(500)), | |
| % force exit | |
| exit(normal); | |
| {HttpCode, Headers0, Body} -> | |
| % received normal response | |
| ?DEBUG(debug, "sending response", []), | |
| % kill listening socket | |
| catch SocketPid ! shutdown, | |
| % flatten body [optimization since needed for content length] | |
| BodyBinary = convert_to_binary(Body), | |
| % provide response | |
| Headers = add_content_length(Headers0, BodyBinary), | |
| Enc_headers = enc_headers(Headers), | |
| Resp = [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>, BodyBinary], | |
| send(Sock, Resp); | |
| {raw, Body} -> | |
| send(Sock, Body); | |
| _ -> | |
| % loop exited normally, kill listening socket | |
| catch SocketPid ! shutdown | |
| end. | |
| % Description: Ensure Body is binary. | |
| convert_to_binary(Body) when is_list(Body) -> | |
| list_to_binary(lists:flatten(Body)); | |
| convert_to_binary(Body) when is_binary(Body) -> | |
| Body; | |
| convert_to_binary(Body) when is_atom(Body) -> | |
| list_to_binary(atom_to_list(Body)). | |
| % Description: Socket loop for stream responses | |
| socket_loop(#c{sock = Sock} = C) -> | |
| receive | |
| {stream_head, HttpCode, Headers} -> | |
| ?DEBUG(debug, "sending stream head", []), | |
| Enc_headers = enc_headers(Headers), | |
| Resp = [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>], | |
| send(Sock, Resp), | |
| socket_loop(C); | |
| {stream_data, Body} -> | |
| ?DEBUG(debug, "sending stream data", []), | |
| send(Sock, Body), | |
| socket_loop(C); | |
| stream_close -> | |
| ?DEBUG(debug, "closing stream", []), | |
| close(Sock); | |
| shutdown -> | |
| ?DEBUG(debug, "shutting down socket loop", []), | |
| shutdown | |
| end. | |
| % Description: Add content length | |
| add_content_length(Headers, Body) -> | |
| case proplists:get_value('Content-Length', Headers) of | |
| undefined -> | |
| [{'Content-Length', size(Body)}|Headers]; | |
| false -> | |
| Headers | |
| end. | |
| % Description: Encode headers | |
| enc_headers([{Tag, Val}|T]) when is_atom(Tag) -> | |
| [atom_to_list(Tag), ": ", enc_header_val(Val), "\r\n"|enc_headers(T)]; | |
| enc_headers([{Tag, Val}|T]) when is_list(Tag) -> | |
| [Tag, ": ", enc_header_val(Val), "\r\n"|enc_headers(T)]; | |
| enc_headers([]) -> | |
| []. | |
| enc_header_val(Val) when is_atom(Val) -> | |
| atom_to_list(Val); | |
| enc_header_val(Val) when is_integer(Val) -> | |
| integer_to_list(Val); | |
| enc_header_val(Val) -> | |
| Val. | |
| % Split the path at the ? | |
| split_at_q_mark([$?|T], Acc) -> | |
| {lists:reverse(Acc), T}; | |
| split_at_q_mark([H|T], Acc) -> | |
| split_at_q_mark(T, [H|Acc]); | |
| split_at_q_mark([], Acc) -> | |
| {lists:reverse(Acc), []}. | |
| % TCP send | |
| send(Sock, Data) -> | |
| ?DEBUG(debug, "sending data: ~p", [Data]), | |
| case gen_tcp:send(Sock, Data) of | |
| ok -> | |
| ok; | |
| {error, _Reason} -> | |
| ?DEBUG(debug, "worker crash: ~p", [_Reason]), | |
| exit(normal) | |
| end. | |
| % TCP close | |
| close(Sock) -> | |
| ?DEBUG(debug, "closing socket", []), | |
| case gen_tcp:close(Sock) of | |
| ok -> | |
| ok; | |
| {error, _Reason} -> | |
| ?DEBUG(debug, "could not close socket: ~p", [_Reason]), | |
| exit(normal) | |
| end. | |
| % ============================ /\ INTERNAL FUNCTIONS ======================================================= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| % ========================================================================================================== | |
| % MISULTIN - Main | |
| % | |
| % >-|-|-(°> | |
| % | |
| % Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>. | |
| % All rights reserved. | |
| % | |
| % BSD License | |
| % | |
| % Redistribution and use in source and binary forms, with or without modification, are permitted provided | |
| % that the following conditions are met: | |
| % | |
| % * Redistributions of source code must retain the above copyright notice, this list of conditions and the | |
| % following disclaimer. | |
| % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and | |
| % the following disclaimer in the documentation and/or other materials provided with the distribution. | |
| % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote | |
| % products derived from this software without specific prior written permission. | |
| % | |
| % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED | |
| % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A | |
| % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | |
| % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
| % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| % POSSIBILITY OF SUCH DAMAGE. | |
| % ========================================================================================================== | |
| -module(misultin_utility). | |
| -vsn('0.3.1'). | |
| % API | |
| -export([get_http_status_code/1]). | |
| % ============================ \/ API ====================================================================== | |
| % Function: HttpStatus | |
| % Description: Returns a complete HTTP header | |
| % most common first | |
| get_http_status_code(200) -> | |
| "HTTP/1.1 200 OK\r\n"; | |
| get_http_status_code(100) -> | |
| "HTTP/1.1 100 Continue\r\n"; | |
| get_http_status_code(101) -> | |
| "HTTP/1.1 101 Switching Protocols\r\n"; | |
| get_http_status_code(301) -> | |
| "HTTP/1.1 301 Moved Permanently\r\n"; | |
| get_http_status_code(400) -> | |
| "HTTP/1.1 400 Bad Request\r\n"; | |
| get_http_status_code(401) -> | |
| "HTTP/1.1 401 Unauthorized\r\n"; | |
| get_http_status_code(403) -> | |
| "HTTP/1.1 403 Forbidden\r\n"; | |
| get_http_status_code(404) -> | |
| "HTTP/1.1 404 Not Found\r\n"; | |
| get_http_status_code(408) -> | |
| "HTTP/1.1 408 Request Timeout\r\n"; | |
| get_http_status_code(500) -> | |
| "HTTP/1.1 500 Internal Server Error\r\n"; | |
| get_http_status_code(501) -> | |
| "HTTP/1.1 501 Not Implemented\r\n"; | |
| % less common last | |
| get_http_status_code(201) -> | |
| "HTTP/1.1 201 Created\r\n"; | |
| get_http_status_code(202) -> | |
| "HTTP/1.1 202 Accepted\r\n"; | |
| get_http_status_code(203) -> | |
| "HTTP/1.1 203 Non-Authoritative Information\r\n"; | |
| get_http_status_code(204) -> | |
| "HTTP/1.1 204 No Content\r\n"; | |
| get_http_status_code(205) -> | |
| "HTTP/1.1 205 Reset Content\r\n"; | |
| get_http_status_code(206) -> | |
| "HTTP/1.1 206 Partial Content\r\n"; | |
| get_http_status_code(300) -> | |
| "HTTP/1.1 300 Multiple Choices\r\n"; | |
| get_http_status_code(302) -> | |
| "HTTP/1.1 302 Found\r\n"; | |
| get_http_status_code(303) -> | |
| "HTTP/1.1 303 See Other\r\n"; | |
| get_http_status_code(304) -> | |
| "HTTP/1.1 304 Not Modified\r\n"; | |
| get_http_status_code(305) -> | |
| "HTTP/1.1 305 Use Proxy\r\n"; | |
| get_http_status_code(307) -> | |
| "HTTP/1.1 307 Temporary Redirect\r\n"; | |
| get_http_status_code(402) -> | |
| "HTTP/1.1 402 Payment Required\r\n"; | |
| get_http_status_code(405) -> | |
| "HTTP/1.1 405 Method Not Allowed\r\n"; | |
| get_http_status_code(406) -> | |
| "HTTP/1.1 406 Not Acceptable\r\n"; | |
| get_http_status_code(407) -> | |
| "HTTP/1.1 407 Proxy Authentication Required\r\n"; | |
| get_http_status_code(409) -> | |
| "HTTP/1.1 409 Conflict\r\n"; | |
| get_http_status_code(410) -> | |
| "HTTP/1.1 410 Gone\r\n"; | |
| get_http_status_code(411) -> | |
| "HTTP/1.1 411 Length Required\r\n"; | |
| get_http_status_code(412) -> | |
| "HTTP/1.1 412 Precondition Failed\r\n"; | |
| get_http_status_code(413) -> | |
| "HTTP/1.1 413 Request Entity Too Large\r\n"; | |
| get_http_status_code(414) -> | |
| "HTTP/1.1 414 Request-URI Too Long\r\n"; | |
| get_http_status_code(415) -> | |
| "HTTP/1.1 415 Unsupported Media Type\r\n"; | |
| get_http_status_code(416) -> | |
| "HTTP/1.1 416 Requested Range Not Satisfiable\r\n"; | |
| get_http_status_code(417) -> | |
| "HTTP/1.1 417 Expectation Failed\r\n"; | |
| get_http_status_code(502) -> | |
| "HTTP/1.1 502 Bad Gateway\r\n"; | |
| get_http_status_code(503) -> | |
| "HTTP/1.1 503 Service Unavailable\r\n"; | |
| get_http_status_code(504) -> | |
| "HTTP/1.1 504 Gateway Timeout\r\n"; | |
| get_http_status_code(505) -> | |
| "HTTP/1.1 505 HTTP Version Not Supported\r\n"; | |
| get_http_status_code(Other) -> | |
| lists:flatten(io_lib:format("HTTP/1.1 ~p \r\n", [Other])). | |
| % ============================ /\ API ====================================================================== | |
| % ============================ \/ INTERNAL FUNCTIONS ======================================================= | |
| % ============================ /\ INTERNAL FUNCTIONS ======================================================= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'rake/clean' | |
| INCLUDE = "include" | |
| ERLC_FLAGS = "-I#{INCLUDE} +warn_unused_vars +warn_unused_import" | |
| SRC = FileList['src/*.erl'] | |
| OBJ = SRC.pathmap("%{src,ebin}X.beam") | |
| CLEAN.include("ebin/*.beam") | |
| directory 'ebin' | |
| rule ".beam" => ["%{ebin,src}X.erl"] do |t| | |
| sh "erlc -D EUNIT -pa ebin -W #{ERLC_FLAGS} -o ebin #{t.source}" | |
| end | |
| task :compile => ['ebin'] + OBJ | |
| task :default => :compile | |
| task :run_tests => [:compile] do | |
| puts "Modules under test:" | |
| OBJ.each do |obj| | |
| obj[%r{.*/(.*).beam}] | |
| mod = $1 | |
| test_output = `erl -pa ebin -run #{mod} test -run init stop` | |
| if /\*failed\*/ =~ test_output | |
| test_output[/(Failed.*Aborted.*Skipped.*Succeeded.*$)/] | |
| else | |
| test_output[/1>\s*(.*)\n/] | |
| end | |
| puts "#{mod}: #{$1}" | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -module(req). | |
| -behaviour(gen_server). | |
| -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). | |
| -export([get/1, | |
| put/3, | |
| first_run/0, | |
| start/0, | |
| stop/0, | |
| md5_hex/1, | |
| start_http/1, | |
| stop_http/0, | |
| handle_http/1, | |
| start_link/0]). | |
| -define(SERVER, global:whereis_name(?MODULE)). | |
| start_link() -> | |
| gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). | |
| stop() -> | |
| gen_server:call(?SERVER, {stop}). | |
| % control misultin http server | |
| start_http(Port) -> | |
| misultin:start_link([{port, Port}, {loop, fun(Req) -> handle_http(Req) end}]). | |
| stop_http() -> | |
| misultin:stop(). | |
| % callback on request received | |
| handle_http(Req) -> | |
| Method = Req:get(method), | |
| {abs_path, "/" ++ Uri} = Req:get(uri), | |
| {match, [{1, BucketLength}]} = regexp:matches(Uri, "^[^/]*"), | |
| Bucket = string:substr(Uri, 1, BucketLength), | |
| Key = case string:len(Uri) > BucketLength + 2 of | |
| true -> | |
| string:substr(Uri, BucketLength + 2); | |
| false -> | |
| none | |
| end, | |
| io:format("Method: ~p Bucket: ~s Key: ~p~n", [Method, Bucket, Key]), | |
| handle(Req, {Method, Bucket, Key}). | |
| handle(Req, {'GET', Bucket, none}) -> | |
| case bucket:fetch(Bucket) of | |
| not_found -> | |
| Req:respond(404, "No Such Bucket"); | |
| _ -> | |
| Req:ok(io_lib:format("<?xml version='1.0' encoding='UTF-8'?><ListBucketResult xmlns='http://s3.amazonaws.com/doc/2006-03-01'><Name>~s</Name><Prefix></Prefix><Marker></Marker><MaxKeys>0</MaxKeys><IsTruncated>false</IsTruncated><Contents></Contents></ListBucketResult>", [Bucket])) | |
| end; | |
| handle(Req, {'GET', Bucket, Key}) -> | |
| case meta:fetch(Bucket, Key) of | |
| not_found -> | |
| Req:respond(404, "404 not found"); | |
| Headers -> | |
| Req:ok(Headers, storage:fetch(Bucket, Key)) | |
| end; | |
| handle(Req, {'DELETE', Bucket, Key}) -> | |
| meta:delete(Bucket, Key), | |
| storage:delete(Bucket, Key), | |
| Req:respond(204, ""); | |
| handle(Req, {'PUT', Bucket, none}) -> | |
| bucket:insert(Bucket, none), | |
| Req:ok("success"); | |
| handle(Req, {'PUT', Bucket, Key}) -> | |
| meta:insert(Bucket, Key, []), | |
| Content = Req:get(body), | |
| storage:insert(Bucket, Key, Content), | |
| MD5 = md5_hex(Content), | |
| Req:ok([{"ETag", "\"" ++ MD5 ++ "\""}], "success"); | |
| handle(Req, {Method, Bucket, Key}) -> | |
| Req:respond(501, io_lib:format("Haven't handled ~p ~p ~p~n", [Method, Bucket, Key])). | |
| get(Object) -> | |
| gen_server:call(?SERVER, {get, Object}, infinity). | |
| put(Object, Headers, Content) -> | |
| gen_server:call(?SERVER, {put, Object, Headers, Content}, infinity). | |
| init([]) -> | |
| {ok, []}. | |
| handle_call({stop}, _From, State) -> | |
| {stop, stop, State}; | |
| handle_call({put, ObjectId, Headers, Content}, _From, State) -> | |
| ok = meta:put(ObjectId, Headers), | |
| ok = storage:put(ObjectId, Content), | |
| {reply, ok, State}; | |
| handle_call({get, ObjectId}, _From, State) -> | |
| Pid = proc_lib:spawn_link(meta, get, [ObjectId]), | |
| File = storage:get(ObjectId), | |
| io:format("Header: ~p~n", [Pid]), | |
| io:format("Content: ~p~n", [File]), | |
| {reply, ok, State}. | |
| handle_cast(_Msg, State) -> {noreply, State}. | |
| handle_info(_Msg, State) -> {noreply, State}. | |
| terminate(_Reason, _State) -> | |
| ok. | |
| code_change(_OldVersion, State, _Extra) -> | |
| io:format("Reloading code for ?MODULE\n",[]), | |
| {ok, State}. | |
| start() -> | |
| meta:start(), | |
| storage:start(), | |
| bucket:start(), | |
| req:start_link(), | |
| req:start_http(1234), | |
| io:format("Setting up on 1234~n"). | |
| first_run() -> | |
| io:format("Building tables~n"), | |
| meta:first_run(), | |
| storage:first_run(), | |
| bucket:first_run(). | |
| md5_hex(S) -> | |
| Md5_bin = erlang:md5(S), | |
| Md5_list = binary_to_list(Md5_bin), | |
| lists:flatten(list_to_hex(Md5_list)). | |
| list_to_hex(L) -> | |
| lists:map(fun(X) -> int_to_hex(X) end, L). | |
| int_to_hex(N) when N < 256 -> | |
| [hex(N div 16), hex(N rem 16)]. | |
| hex(N) when N < 10 -> | |
| $0+N; | |
| hex(N) when N >= 10, N < 16 -> | |
| $a + (N-10). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {application, s2, | |
| [ | |
| {description, "S2: Stupid S3"}, | |
| {vsn, '0.0.1'}, | |
| {modules, [req, bucket, storage, meta]}, | |
| {registered, [req]}, | |
| {env, []}, | |
| {applications, [kernel, stdlib]} | |
| ]}. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -module(storage). | |
| -export([insert/3, | |
| fetch/2, | |
| delete/2, | |
| first_run/0, | |
| start/0, | |
| stop/0]). | |
| -record(file, {index, content}). | |
| start() -> | |
| ok = mnesia:start(), | |
| io:format("Waiting on mnesia tables..\n",[]), | |
| mnesia:wait_for_tables([file], 30000), | |
| mnesia:table_info(file, all), | |
| ok. | |
| stop() -> | |
| mnesia:stop(). | |
| first_run() -> | |
| mnesia:create_schema([node()]), | |
| ok = mnesia:start(), | |
| mnesia:create_table(file, | |
| [ {disc_copies, [node()] }, | |
| {attributes, | |
| record_info(fields,file)} ]). | |
| fetch(Bucket, Key) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Fun = fun() -> | |
| mnesia:read({file, Id}) | |
| end, | |
| case mnesia:transaction(Fun) of | |
| {atomic, []} -> | |
| not_found; | |
| {atomic, [File]} -> | |
| File#file.content | |
| end. | |
| insert(Bucket, Key, Content) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Fun = fun() -> | |
| mnesia:write( | |
| #file{ index = Id, | |
| content = Content } ) | |
| end, | |
| {atomic, Result} = mnesia:transaction(Fun), | |
| Result. | |
| delete(Bucket, Key) -> | |
| Id = Bucket ++ "/" ++ Key, | |
| Delete=#file{ index = Id, _ = '_'}, | |
| Fun = fun() -> | |
| List = mnesia:match_object(Delete), | |
| lists:foreach(fun(X) -> | |
| mnesia:delete_object(X) | |
| end, List) | |
| end, | |
| mnesia:transaction(Fun). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/ | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a | |
| # copy of this software and associated documentation files (the | |
| # "Software"), to deal in the Software without restriction, including | |
| # without limitation the rights to use, copy, modify, merge, publish, dis- | |
| # tribute, sublicense, and/or sell copies of the Software, and to permit | |
| # persons to whom the Software is furnished to do so, subject to the fol- | |
| # lowing conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included | |
| # in all copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | |
| # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | |
| # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| # IN THE SOFTWARE. | |
| """ | |
| do the unit tests! | |
| """ | |
| import sys, os, unittest | |
| import getopt, sys | |
| import boto | |
| from boto_test import S3ConnectionTest | |
| def usage(): | |
| print 'test.py [-t testsuite] [-v verbosity]' | |
| print ' -t run specific testsuite (s3|sqs|ec2|sdb|all)' | |
| print ' -v verbosity (0|1|2)' | |
| def main(): | |
| try: | |
| opts, args = getopt.getopt(sys.argv[1:], 'ht:v:', | |
| ['help', 'testsuite', 'verbosity']) | |
| except: | |
| usage() | |
| sys.exit(2) | |
| testsuite = 'all' | |
| verbosity = 1 | |
| for o, a in opts: | |
| if o in ('-h', '--help'): | |
| usage() | |
| sys.exit() | |
| if o in ('-t', '--testsuite'): | |
| testsuite = a | |
| if o in ('-v', '--verbosity'): | |
| verbosity = int(a) | |
| if len(args) != 0: | |
| usage() | |
| sys.exit() | |
| suite = unittest.TestSuite() | |
| suite.addTest(unittest.makeSuite(S3ConnectionTest)) | |
| unittest.TextTestRunner(verbosity=verbosity).run(suite) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment