anotherjesse (owner)

Revisions

gist: 214961 Download_button fork
public
Description:
s2 - stupid s3
Public Clone URL: git://gist.github.com/214961.git
Embed All Files: show embed
.gitignore #
1
2
3
*.pyc
*.beam
 
compile #
1
2
3
4
5
6
7
8
9
10
11
12
13
# 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."
 
include/misultin.hrl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
% ==========================================================================================================
% 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
}).
 
rakefile.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
 
src/bucket.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-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.
 
src/meta.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
-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).
 
src/misultin.app #
1
2
3
4
5
6
7
8
9
10
{application, misultin,
[
{description, "Lightweight HTTP Server Library"},
{vsn, '0.3'},
{modules, [misultin_socket, misultin_req, misultin]},
{registered, [misultin]},
{env, []},
{applications, [kernel, stdlib]}
]}.
 
src/misultin.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
% ==========================================================================================================
% 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 =======================================================
 
src/misultin_req.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
% ==========================================================================================================
% 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 =======================================================
 
src/misultin_socket.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
% ==========================================================================================================
% 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 =======================================================
 
src/misultin_utility.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
% ==========================================================================================================
% 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 =======================================================
 
src/req.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
-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).
 
src/s2.app #
1
2
3
4
5
6
7
8
9
10
{application, s2,
[
  {description, "S2: Stupid S3"},
  {vsn, '0.0.1'},
  {modules, [req, bucket, storage, meta]},
  {registered, [req]},
  {env, []},
  {applications, [kernel, stdlib]}
]}.
 
src/storage.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
-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).
 
tests/boto_test.py #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/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 ---'
 
tests/test.py #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/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()