1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2000-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21
22-module(httpd_poll).
23-behaviour(gen_server).
24
25
26%% External API
27-export([start/0, start_appup/2, start/3,stop/0,verbosity/1,poll_time/1]).
28
29%% gen_server exports
30-export([init/1,
31	 handle_call/3, handle_cast/2, handle_info/2, terminate/2,
32	 code_change/3]).
33
34
35-define(default_verbosity,error).
36-define(default_poll_time,60000). %% 60 seconds
37
38
39-record(state,{host = "", port = -1, ptime = -1, tref = none, uris = []}).
40
41
42%% start/0
43%%
44%% Description: Start polling HTTPD with default values
45%%
46start() ->
47    Options = default_options(otp),
48    start("gandalf", 8000, Options).
49
50start_appup(Host, Port) ->
51    Options = default_options(top),
52    start(Host, Port, Options).
53
54%% start/3
55%%
56%% Description: Start polling HTTPD
57%%
58%% Parameters:
59%%              Host        = string()
60%%                            Host name of HTTPD
61%%              Port        = integer()
62%%                            Port number of HTTPD
63%%              Options     = [Option]
64%%              Option      = {poll_time,integer()} | {verbosity,verbosity()} |
65%%                            {log_file,string()}   | {uris,[uri()]}
66%%              verbosity() = silence | error | log | debug | trace
67%%              uri()       = {string(),string}
68%%                            First part is a descriptive string and the second
69%%                            part is the actual URI.
70%%
71start(Host,Port,Options) ->
72    gen_server:start({local,httpd_tester},?MODULE,[Host,Port,Options],[]).
73
74stop() ->
75    gen_server:call(httpd_tester,stop).
76
77
78default_options(UriDesc) ->
79    Verbosity = {verbosity,?default_verbosity},
80    Uris      = {uris,uris(UriDesc)},
81    PollTime  = {poll_time,?default_poll_time},
82    Logging   = {log_file,"httpd_poll.log"},
83    [Verbosity, Uris, PollTime, Logging].
84
85
86options(Options) ->
87    options(Options, default_options(otp), []).
88
89options([], Defaults, Options) ->
90    Options ++ Defaults;
91options([{Key, _Val} = Opt|Opts], Defaults, Options) ->
92    options(Opts, lists:keydelete(Key, 1, Defaults), [Opt | Options]).
93
94
95verbosity(silence) ->
96    set_verbosity(silence);
97verbosity(error) ->
98    set_verbosity(error);
99verbosity(log) ->
100    set_verbosity(log);
101verbosity(debug) ->
102    set_verbosity(debug);
103verbosity(trace) ->
104    set_verbosity(trace).
105
106set_verbosity(Verbosity) ->
107    gen_server:cast(httpd_tester,{verbosity,Verbosity}).
108
109poll_time(NewTime) ->
110    gen_server:call(httpd_tester,{poll_time,NewTime}).
111
112
113%% ----------------------------------------------------------------------
114
115
116init([Host, Port, Options0]) ->
117    process_flag(trap_exit,true),
118    Options = options(Options0),
119    put(verbosity,get_verbosity(Options)),
120    log_open(get_log_file(Options)),
121    tstart(),
122    PollTime = get_poll_time(Options),
123    Ref = tcreate(PollTime),
124    log("created"),
125    {ok,#state{host  = Host,
126	       port  = Port,
127	       ptime = PollTime,
128	       tref  = Ref,
129	       uris  = get_uris(Options)}}.
130
131uris(top) ->
132    [uri_top_index()];
133
134uris(otp) ->
135    [
136     uri_top_index(),
137     uri_internal_product1(),
138     uri_internal_product2(),
139     uri_r13b03_test_results(),
140     uri_bjorn1(),
141     uri_bjorn2()
142    ].
143
144uri_top_index() ->
145    {"top page","/"}.
146
147uri_internal_product1() ->
148    {"product internal page (1)","/product/internal/"}.
149
150uri_internal_product2() ->
151    {"product internal page (2)","/product/internal"}.
152
153uri_r13b03_test_results() ->
154    {"daily build index page",
155     "/product/internal/test/daily/logs.html"}.
156
157uri_bjorn1() ->
158    {"bjorns home page (1)","/~bjorn/"}.
159
160uri_bjorn2() ->
161    {"bjorns home page (2)","/~bjorn"}.
162
163
164handle_call(stop, _From, State) ->
165    vlog("stop request"),
166    {stop, normal, ok, State};
167
168handle_call({poll_time,NewTime}, _From, State) ->
169    vlog("set new poll time: ~p",[NewTime]),
170    OldTime = State#state.ptime,
171    {stop, normal, OldTime, State#state{ptime = NewTime}};
172
173handle_call(Request, _From, State) ->
174    vlog("unexpected request(call): ~p",[Request]),
175    {reply, ok, State}.
176
177
178handle_cast({verbosity,Verbosity}, State) ->
179    vlog("set (new) verbosity to: ~p",[Verbosity]),
180    put(verbosity,Verbosity),
181    {noreply, State};
182
183handle_cast(Message, State) ->
184    vlog("unexpected message(call): ~p",[Message]),
185    {noreply, State}.
186
187
188handle_info(poll_time,State) ->
189    {{Description,Uri},Uris} = get_uri(State#state.uris),
190    vlog("poll time for ~s",[Description]),
191    do_poll(State#state.host,State#state.port,Uri),
192    Ref = tcreate(State#state.ptime),
193    {noreply, State#state{tref = Ref, uris = Uris}};
194
195handle_info(Info, State) ->
196    vlog("unexpected message(info): ~p",[Info]),
197    {noreply, State}.
198
199
200code_change(_OldVsn, State, _Extra) ->
201    {ok, State}.
202
203
204terminate(_Reason, State) ->
205    tcancel(State#state.tref),
206    log_close(get(log_file)),
207    ok.
208
209
210get_uri([Uri|Uris]) ->
211    {Uri,Uris++[Uri]}.
212
213
214do_poll(Host,Port,Uri) ->
215    (catch poll(create(Host,Port),Uri,"200")).
216
217poll({ok,Socket},Uri,ExpStatus) ->
218    vtrace("poll -> entry with Socket: ~p",[Socket]),
219    put(latest_requested_uri,Uri),
220    Req = "GET " ++ Uri ++ " HTTP/1.0\r\n\r\n",
221    await_poll_response(send(Socket,Req),Socket,ExpStatus);
222poll({error,Reason},_Req,_ExpStatus) ->
223    verror("failed creating socket: ~p",[Reason]),
224    log("failed creating socket: ~p",[Reason]),
225    exit({error,Reason});
226poll(O,_Req,_ExpStatus) ->
227    verror("unexpected result from socket create: ~p",[O]),
228    log("unexpected result from socket create: ~p",[O]),
229    exit({unexpected_result,O}).
230
231await_poll_response(ok,Socket,ExpStatusCode) ->
232    vtrace("await_poll_response -> awaiting response with status ~s",
233	   [ExpStatusCode]),
234    receive
235	{tcp_closed,Socket} ->
236	    verror("connection closed when awaiting poll response"),
237	    log("connection closed when awaiting reply to GET of '~s'",
238		[get(latest_requested_uri)]),
239	    exit(connection_closed);
240	{tcp,Socket,Response} ->
241	    vdebug("received response"),
242	    validate(ExpStatusCode,Socket,Response)
243    after 10000 ->
244	    verror("connection timeout waiting for poll response",[]),
245	    log("connection timeout waiting for reply to GET of '~s'",
246		[get(latest_requested_uri)]),
247	    exit(connection_timed_out)
248    end;
249await_poll_response(Error,_Socket,_ExpStatusCode) ->
250    verror("failed sending GET request for '~s' for reason: ~p",
251	   [get(latest_requested_uri),Error]),
252    log("failed sending GET request for '~s' for reason: ~p",
253	[get(latest_requested_uri),Error]),
254    exit(Error).
255
256
257validate(ExpStatusCode,Socket,Response) ->
258    Sz = sz(Response),
259    vtrace("validate -> Entry with ~p bytes response",[Sz]),
260    Size = trash_the_rest(Socket,Sz),
261    close(Socket),
262    case re:split(Response," ", [{return, list}]) of
263	["HTTP/1.0",ExpStatusCode|_] ->
264	    vlog("response (~p bytes) was ok",[Size]),
265	    ok;
266	["HTTP/1.0",StatusCode|_] ->
267	    verror("unexpected response status received: ~s => ~s",
268		   [StatusCode,status_to_message(StatusCode)]),
269	    log("unexpected result to GET of '~s': ~s => ~s",
270		[get(latest_requested_uri),StatusCode,
271		 status_to_message(StatusCode)]),
272	    exit({unexpected_response_code,StatusCode,ExpStatusCode})
273    end.
274
275
276%% ------------------------------------------------------------------
277
278trash_the_rest(Socket,N) ->
279    receive
280	{tcp, Socket, Trash} ->
281	    vtrace("trash_the_rest -> trash ~p bytes",[sz(Trash)]),
282	    trash_the_rest(Socket,add(N,sz(Trash)));
283	{tcp_closed, Socket} ->
284	    vdebug("socket closed after receiving ~p bytes",[N]),
285	    N
286    after 10000 ->
287	    verror("connection timeout waiting for message"),
288	    exit(connection_timed_out)
289    end.
290
291
292add(N1, N2) when is_integer(N1) andalso is_integer(N2) ->
293    N1 + N2;
294add(N1, _N2) when is_integer(N1) ->
295    N1;
296add(_N1, N2) when is_integer(N2) ->
297    N2.
298
299sz(L) when is_list(L) ->
300    length(lists:flatten(L));
301sz(B) when is_binary(B) ->
302    size(B);
303sz(O) ->
304    {unknown_size,O}.
305
306
307%% --------------------------------------------------------------
308%%
309%% Status code to printable string
310%%
311
312status_to_message(L) when is_list(L) ->
313    case (catch list_to_integer(L)) of
314	I when is_integer(I) ->
315	    status_to_message(I);
316	_ ->
317	    io_lib:format("UNKNOWN STATUS CODE: '~p'",[L])
318    end;
319status_to_message(100) -> "Section 10.1.1: Continue";
320status_to_message(101) -> "Section 10.1.2: Switching Protocols";
321status_to_message(200) -> "Section 10.2.1: OK";
322status_to_message(201) -> "Section 10.2.2: Created";
323status_to_message(202) -> "Section 10.2.3: Accepted";
324status_to_message(203) -> "Section 10.2.4: Non-Authoritative Information";
325status_to_message(204) -> "Section 10.2.5: No Content";
326status_to_message(205) -> "Section 10.2.6: Reset Content";
327status_to_message(206) -> "Section 10.2.7: Partial Content";
328status_to_message(300) -> "Section 10.3.1: Multiple Choices";
329status_to_message(301) -> "Section 10.3.2: Moved Permanently";
330status_to_message(302) -> "Section 10.3.3: Found";
331status_to_message(303) -> "Section 10.3.4: See Other";
332status_to_message(304) -> "Section 10.3.5: Not Modified";
333status_to_message(305) -> "Section 10.3.6: Use Proxy";
334status_to_message(307) -> "Section 10.3.8: Temporary Redirect";
335status_to_message(400) -> "Section 10.4.1: Bad Request";
336status_to_message(401) -> "Section 10.4.2: Unauthorized";
337status_to_message(402) -> "Section 10.4.3: Peyment Required";
338status_to_message(403) -> "Section 10.4.4: Forbidden";
339status_to_message(404) -> "Section 10.4.5: Not Found";
340status_to_message(405) -> "Section 10.4.6: Method Not Allowed";
341status_to_message(406) -> "Section 10.4.7: Not Acceptable";
342status_to_message(407) -> "Section 10.4.8: Proxy Authentication Required";
343status_to_message(408) -> "Section 10.4.9: Request Time-Out";
344status_to_message(409) -> "Section 10.4.10: Conflict";
345status_to_message(410) -> "Section 10.4.11: Gone";
346status_to_message(411) -> "Section 10.4.12: Length Required";
347status_to_message(412) -> "Section 10.4.13: Precondition Failed";
348status_to_message(413) -> "Section 10.4.14: Request Entity Too Large";
349status_to_message(414) -> "Section 10.4.15: Request-URI Too Large";
350status_to_message(415) -> "Section 10.4.16: Unsupported Media Type";
351status_to_message(416) -> "Section 10.4.17: Requested range not satisfiable";
352status_to_message(417) -> "Section 10.4.18: Expectation Failed";
353status_to_message(500) -> "Section 10.5.1: Internal Server Error";
354status_to_message(501) -> "Section 10.5.2: Not Implemented";
355status_to_message(502) -> "Section 10.5.3: Bad Gatteway";
356status_to_message(503) -> "Section 10.5.4: Service Unavailable";
357status_to_message(504) -> "Section 10.5.5: Gateway Time-out";
358status_to_message(505) -> "Section 10.5.6: HTTP Version not supported";
359status_to_message(Code) -> io_lib:format("Unknown status code: ~p",[Code]).
360
361
362%% ----------------------------------------------------------------
363
364create(Host,Port) ->
365    vtrace("create -> ~n\tHost: ~s~n\tPort: ~p",[Host,Port]),
366    case gen_tcp:connect(Host,Port,[{packet,0},{reuseaddr,true}]) of
367	{ok,Socket} ->
368	    {ok,Socket};
369	{error,{enfile,_}} ->
370	    {error,enfile};
371	Error ->
372	    Error
373    end.
374
375close(Socket) ->
376    gen_tcp:close(Socket).
377
378
379send(Socket,Data) ->
380    vtrace("send -> send ~p bytes of data",[length(Data)]),
381    gen_tcp:send(Socket,Data).
382
383
384%% ----------------------------------------------------------------
385
386tstart() ->
387    timer:start().
388
389tcreate(Time) ->
390    {ok,Ref} = timer:send_after(Time,poll_time),
391    Ref.
392
393tcancel(Ref) ->
394    timer:cancel(Ref).
395
396%% ----------------------------------------------------------------
397
398log_open(undefined) ->
399    ok;
400log_open(FileName) ->
401    put(log_file,fopen(FileName)).
402
403log_close(undefined) ->
404    ok;
405log_close(Fd) ->
406    fclose(Fd).
407
408log(F) ->
409    log(F,[]).
410
411log(F,A) ->
412    {{Year,Month,Day},{Hour,Min,Sec}} = local_time(),
413    fwrite(get(log_file),
414	   "~w.~w.~w ~w.~w.~w " ++ F ++ "~n",
415	   [Year,Month,Day,Hour,Min,Sec] ++ A).
416
417%% ----------------------------------------------------------------
418
419fopen(Name) ->
420    {ok,Fd} = file:open(Name,[write]),
421    Fd.
422
423fclose(Fd) ->
424    file:close(Fd).
425
426fwrite(undefined,_F,_A) ->
427    ok;
428fwrite(Fd,F,A) ->
429    io:format(Fd,F,A).
430
431
432%% ----------------------------------------------------------------
433
434get_poll_time(Opts) ->
435    get_option(poll_time,Opts,?default_poll_time).
436
437get_log_file(Opts) ->
438    get_option(log_file,Opts).
439
440get_uris(Opts) ->
441    get_option(uris,Opts,[]).
442
443get_verbosity(Opts) ->
444    get_option(verbosity,Opts,?default_verbosity).
445
446get_option(Opt,Opts) ->
447    get_option(Opt,Opts,undefined).
448
449get_option(Opt,Opts,Default) ->
450    case lists:keysearch(Opt,1,Opts) of
451	{value,{Opt,Value}} ->
452	    Value;
453	false ->
454	    Default
455    end.
456
457%% ----------------------------------------------------------------
458
459%% sleep(T) -> receive after T -> ok end.
460
461%% ----------------------------------------------------------------
462
463%% vtrace(F)   -> vprint(get(verbosity),trace,F,[]).
464vtrace(F,A) -> vprint(get(verbosity),trace,F,A).
465
466vdebug(F)   -> vprint(get(verbosity),debug,F,[]).
467vdebug(F,A) -> vprint(get(verbosity),debug,F,A).
468
469vlog(F)     -> vprint(get(verbosity),log,F,[]).
470vlog(F,A)   -> vprint(get(verbosity),log,F,A).
471
472verror(F)   -> vprint(get(verbosity),error,F,[]).
473verror(F,A) -> vprint(get(verbosity),error,F,A).
474
475vprint(trace, Severity,  F,  A)    -> vprint(Severity,F,A);
476vprint(debug, trace,    _F, _A)    -> ok;
477vprint(debug, Severity,  F,  A)    -> vprint(Severity,F,A);
478vprint(log,   log,       F,  A)    -> vprint(log,F,A);
479vprint(log,   error,     F,  A)    -> vprint(log,F,A);
480vprint(error, error,     F,  A)    -> vprint(error,F,A);
481vprint(_Verbosity,_Severity,_F,_A) -> ok.
482
483vprint(Severity,F,A) ->
484    {{Year,Month,Day},{Hour,Min,Sec}} = local_time(),
485    io:format("~w.~w.~w ~w.~w.~w " ++ image_of(Severity) ++ F ++ "~n",
486	      [Year,Month,Day,Hour,Min,Sec] ++ A).
487
488image_of(error) -> "ERR: ";
489image_of(log)   -> "LOG: ";
490image_of(debug) -> "DBG: ";
491image_of(trace) -> "TRC: ".
492
493local_time() -> calendar:local_time().
494
495
496