Skip to content

Instantly share code, notes, and snippets.

@anotherjesse
Created October 21, 2009 08:12
Show Gist options
  • Select an option

  • Save anotherjesse/214961 to your computer and use it in GitHub Desktop.

Select an option

Save anotherjesse/214961 to your computer and use it in GitHub Desktop.
s2 - stupid s3
*.pyc
*.beam
#!/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 ---'
-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.
# 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."
-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).
{application, misultin,
[
{description, "Lightweight HTTP Server Library"},
{vsn, '0.3'},
{modules, [misultin_socket, misultin_req, misultin]},
{registered, [misultin]},
{env, []},
{applications, [kernel, stdlib]}
]}.
% ==========================================================================================================
% 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 =======================================================
% ==========================================================================================================
% 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
}).
% ==========================================================================================================
% 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 =======================================================
% ==========================================================================================================
% 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 =======================================================
% ==========================================================================================================
% 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 =======================================================
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
-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).
{application, s2,
[
{description, "S2: Stupid S3"},
{vsn, '0.0.1'},
{modules, [req, bucket, storage, meta]},
{registered, [req]},
{env, []},
{applications, [kernel, stdlib]}
]}.
-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).
#!/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