edouard-clarity 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ require 'uri'
2
+ require 'erb'
3
+
4
+ class LogRenderer
5
+
6
+ # Thank you to http://daringfireball.net/2009/11/liberal_regex_for_matching_urls
7
+ #
8
+ UrlParser = %r{\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))}
9
+ Prefix = ""
10
+ Suffix = "<br/>\n"
11
+
12
+ def render(line = {})
13
+ # Escape
14
+ output = ERB::Util.h(line)
15
+
16
+ # Transform urls into html links
17
+ output.gsub!(UrlParser) do |match|
18
+ html_link(match)
19
+ end
20
+
21
+ # Return with formatting
22
+ "#{Prefix}#{output}#{Suffix}"
23
+ end
24
+
25
+ def finalize
26
+ '</div><hr><p id="done">Done</p></body></html>'
27
+ end
28
+
29
+ private
30
+
31
+ def html_link(url)
32
+ uri = URI.parse(url) rescue url
33
+ "<a href='#{uri}'>#{url}</a>"
34
+ end
35
+
36
+ end
@@ -0,0 +1,139 @@
1
+ require 'cgi'
2
+ require File.dirname(__FILE__) + '/server/basic_auth'
3
+ require File.dirname(__FILE__) + '/server/mime_types'
4
+ require File.dirname(__FILE__) + '/server/chunk_http'
5
+ require File.dirname(__FILE__) + '/grep_renderer'
6
+ require File.dirname(__FILE__) + '/process_tree'
7
+
8
+ module Clarity
9
+ class NotFoundError < StandardError; end
10
+ class NotAuthenticatedError < StandardError; end
11
+ class InvalidParameterError < StandardError; end
12
+
13
+ module Server
14
+ include EventMachine::HttpServer
15
+ include Clarity::BasicAuth
16
+ include Clarity::ChunkHttp
17
+
18
+ attr_accessor :required_username, :required_password, :relative_root
19
+ attr_accessor :log_files
20
+
21
+ def self.run(options)
22
+
23
+ EventMachine::run do
24
+ EventMachine.epoll
25
+ EventMachine::start_server(options[:address], options[:port], self) do |a|
26
+ a.log_files = options[:log_files]
27
+ a.required_username = options[:username]
28
+ a.required_password = options[:password]
29
+ a.relative_root = options[:relative_root] || ""
30
+ end
31
+
32
+ STDERR.puts "Clarity #{Clarity::VERSION} starting up."
33
+ STDERR.puts " * listening on #{options[:address]}:#{options[:port]}"
34
+
35
+ if options[:user]
36
+ STDERR.puts " * Running as user #{options[:user]}"
37
+ EventMachine.set_effective_user(options[:user])
38
+ end
39
+
40
+ STDERR.puts " * Log mask(s): #{options[:log_files].join(', ')}"
41
+
42
+ if options[:username].nil? or options[:password].nil?
43
+ STDERR.puts " * WARNING: No username/password specified. This is VERY insecure."
44
+ end
45
+
46
+ STDERR.puts
47
+
48
+
49
+ end
50
+ end
51
+
52
+ def process_http_request
53
+ authenticate!
54
+
55
+ puts "action: #{path}"
56
+ puts "params: #{params.inspect}"
57
+
58
+ @hostname = HostnameCommandBuilder.command
59
+
60
+ case path
61
+ when '/'
62
+ respond_with(200, welcome_page)
63
+
64
+ when '/perform'
65
+ if params.empty?
66
+ respond_with(200, welcome_page)
67
+ else
68
+ # get command
69
+ command = case params['tool']
70
+ when 'grep' then GrepCommandBuilder.new(params).command
71
+ when 'tail' then TailCommandBuilder.new(params).command
72
+ else raise InvalidParameterError, "Invalid Tool parameter"
73
+ end
74
+ response = respond_with_chunks
75
+ response.chunk results_page # display page header
76
+
77
+ puts "Running: #{command}"
78
+
79
+ EventMachine::popen(command, GrepRenderer) do |grepper|
80
+ @grepper = grepper
81
+ @grepper.response = response
82
+ end
83
+ end
84
+
85
+ when '/test'
86
+ response = respond_with_chunks
87
+ EventMachine::add_periodic_timer(1) do
88
+ response.chunk "Lorem ipsum dolor sit amet<br/>"
89
+ response.send_chunks
90
+ end
91
+
92
+ else
93
+ respond_with(200, public_file(path), :content_type => Mime.for(path))
94
+ end
95
+
96
+ rescue InvalidParameterError => e
97
+ respond_with(500, error_page(e))
98
+ rescue NotFoundError => e
99
+ respond_with(404, "<h1>Not Found</h1>")
100
+ rescue NotAuthenticatedError => e
101
+ puts "Could not authenticate user"
102
+ headers = { "WWW-Authenticate" => %(Basic realm="Clarity")}
103
+ respond_with(401, "HTTP Basic: Access denied.\n", :content_type => 'text/plain', :headers => headers)
104
+ end
105
+
106
+ def error_page(error)
107
+ @error = error
108
+ render "error.html.erb"
109
+ end
110
+
111
+ def welcome_page
112
+ render "index.html.erb"
113
+ end
114
+
115
+ def results_page
116
+ render "index.html.erb"
117
+ end
118
+
119
+ def unbind
120
+ @grepper.close_connection if @grepper
121
+ close_connection
122
+ end
123
+
124
+ private
125
+
126
+ def authenticate!
127
+ login, pass = authentication_data
128
+
129
+ if (required_username && required_username != login) || (required_password && required_password != pass)
130
+ raise NotAuthenticatedError
131
+ end
132
+
133
+ true
134
+ end
135
+
136
+ end
137
+
138
+
139
+ end
@@ -0,0 +1,24 @@
1
+ module Clarity
2
+ module BasicAuth
3
+
4
+ def decode_credentials(request)
5
+ Base64.decode64(request).split.last
6
+ end
7
+
8
+ def user_name_and_password(request)
9
+ decode_credentials(request).split(/:/, 2)
10
+ end
11
+
12
+ def authentication_data
13
+ headers = @http_headers.split("\000")
14
+ auth_header = headers.detect {|head| head =~ /Authorization: / }
15
+ header = auth_header.nil? ? "" : auth_header.split("Authorization: Basic ").last
16
+ return (user_name_and_password(header) rescue ['', ''])
17
+ end
18
+
19
+ def authenticate!(http_header)
20
+ raise NotAuthenticatedError unless authenticate(http_header)
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ require 'erb'
2
+
3
+ module Clarity
4
+
5
+ module ChunkHttp
6
+
7
+ LeadIn = ' ' * 1024
8
+
9
+ def respond_with_chunks
10
+ response = EventMachine::DelegatedHttpResponse.new( self )
11
+ response.status = 200
12
+ response.headers['Content-Type'] = 'text/html'
13
+ response.chunk LeadIn
14
+ response
15
+ end
16
+
17
+ def respond_with(status, content, options = {})
18
+ response = EventMachine::DelegatedHttpResponse.new( self )
19
+ response.headers['Content-Type'] = options.fetch(:content_type, 'text/html')
20
+ response.headers['Cache-Control'] = 'private, max-age=0'
21
+ headers = options.fetch(:headers, {})
22
+ headers.each_pair {|h, v| response.headers[h] = v }
23
+ response.status = status
24
+ response.content = content
25
+ response.send_response
26
+ end
27
+
28
+ def render(view)
29
+ @toolbar = template("_toolbar.html.erb")
30
+ @content_for_header = template("_header.html.erb")
31
+ template(view)
32
+ end
33
+
34
+ def template(filename)
35
+ content = File.read( File.join(Clarity::Templates, filename) )
36
+ ERB.new(content).result(binding)
37
+ end
38
+
39
+ def public_file(filename)
40
+ File.read( File.join(Clarity::Public, filename) )
41
+ rescue Errno::ENOENT
42
+ raise NotFoundError
43
+ end
44
+
45
+ def logfiles
46
+ log_files.map {|f| Dir[f] }.flatten.compact.uniq.select{|f| File.file?(f) }.sort
47
+ end
48
+
49
+ def params
50
+ ENV['QUERY_STRING'].split('&').inject({}) {|p,s| k,v = s.split('=');p[k.to_s] = CGI.unescape(v.to_s);p}
51
+ end
52
+
53
+ def path
54
+ ENV["PATH_INFO"]
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,23 @@
1
+ module Clarity
2
+ module Mime
3
+ def self.for(filename)
4
+
5
+ content_type = TYPES[File.extname(filename)]
6
+ content_type || 'text/plain'
7
+ end
8
+
9
+ TYPES = {
10
+ '.jpg' => 'image/jpg',
11
+ '.jpeg' => 'image/jpeg',
12
+ '.gif' => 'image/gif',
13
+ '.png' => 'image/png',
14
+ '.bmp' => 'image/bmp',
15
+ '.bitmap' => 'image/x-ms-bmp',
16
+ '.js' => 'application/javascript',
17
+ '.txt' => 'text/plain',
18
+ '.css' => 'text/css',
19
+ '.html' => 'text/html',
20
+ '.htm' => 'text/html'
21
+ }
22
+ end
23
+ end
Binary file
@@ -0,0 +1,154 @@
1
+ /* ----------------------------------------------------------------------------
2
+ * Log Search Server JS
3
+ * by John Tajima
4
+ * requires jQuery 1.3.2
5
+ * ----------------------------------------------------------------------------
6
+ */
7
+
8
+
9
+ var Search = {
10
+ search_form : 'search', // domId of the form
11
+ resultsId : 'results',
12
+ search_fields: [ 'term1', 'term2', 'term3' ], // domIds of search term fields
13
+ file_list : 'file-list', // domId of select for logfiles
14
+ logfiles : {}, // hash of log files
15
+ past_params : null, // recent request
16
+ url : '/perform',
17
+ scroll_fnId : null,
18
+
19
+ // initialize Search form
20
+ // { 'grep': [ log, files, for, grep], 'tail': [ 'log', 'files', 'for', 'tail']}
21
+ init: function(logfiles, params) {
22
+ this.logfiles = logfiles;
23
+ this.past_params = params;
24
+
25
+ this.bind_grep_tool();
26
+ this.bind_tail_tool();
27
+ this.bind_options();
28
+
29
+ if (!this.past_params) return; // return if no prev settings, nothing to set
30
+
31
+ // init tool selector
32
+ (this.past_params['tool'] == 'grep') ? $('#grep-label').trigger('click') : $('#tail-tool').trigger('click');
33
+
34
+ // init log file selector
35
+ $('#'+this.file_list).val(this.past_params['file']);
36
+
37
+ // init search fields
38
+ jQuery.each(this.search_fields, function(){
39
+ $('#'+this).val(Search.past_params[this]);
40
+ });
41
+
42
+ // advanced options usd?
43
+ // time was set - so show advanced options
44
+ if ((this.past_params['sh']) || (this.past_params['eh'])) {
45
+ this.showAdvanced();
46
+ if (this.past_params['sh']) {
47
+ jQuery.each(['sh', 'sm', 'ss'], function(){ $('#'+this).val(Search.past_params[this]) });
48
+ }
49
+ if (this.past_params['eh']) {
50
+ jQuery.each(['eh', 'em', 'es'], function(){ $('#'+this).val(Search.past_params[this]) });
51
+ }
52
+ }
53
+
54
+ },
55
+
56
+ // bind option selectors
57
+ bind_options: function() {
58
+ $('#auto-scroll').bind('change', function(){
59
+ Search.autoScroll(this.checked);
60
+ });
61
+ $('#auto-scroll').attr('checked', true).trigger('change'); // by default, turn on
62
+ },
63
+
64
+ // bind change grep tool
65
+ bind_grep_tool: function() {
66
+ $('#grep-tool').bind('change', function(e){
67
+ var newlist = ""
68
+ jQuery.each(Search.logfiles['grep'], function(){
69
+ newlist += "<option value='" + this + "'>" + this + "</option>\n"
70
+ });
71
+ $('#'+Search.file_list).html(newlist);
72
+ });
73
+ // watch clicking label as well
74
+ $('#grep-label').bind('click', function(e){
75
+ $('#grep-tool').attr('checked', 'checked').val('grep').trigger('change');
76
+ });
77
+ },
78
+
79
+
80
+ // bind change tail tool
81
+ bind_tail_tool: function() {
82
+ $('#tail-tool').bind('change', function(e){
83
+ var newlist = ""
84
+ jQuery.each(Search.logfiles['tail'], function(){
85
+ newlist += "<option value='" + this + "'>" + this + "</option>\n"
86
+ });
87
+ $('#'+ Search.file_list).html(newlist);
88
+ });
89
+ // watch clicking label as well
90
+ $('#tail-label').bind('click', function(e){
91
+ $('#tail-tool').attr('checked', 'checked').val('tail').trigger('change');
92
+ });
93
+ },
94
+
95
+
96
+ // clears the terms fields
97
+ clear: function() {
98
+ jQuery.each(this.search_fields, function(){
99
+ $('#'+this).val("");
100
+ });
101
+ },
102
+
103
+ showAdvanced: function() {
104
+ $('#enable-advanced').hide();
105
+ $('#disable-advanced').show();
106
+ $('.advanced-options').show();
107
+ },
108
+
109
+ hideAdvanced: function() {
110
+ this.clearAdvanced();
111
+ $('#enable-advanced').show();
112
+ $('#disable-advanced').hide();
113
+ $('.advanced-options').hide();
114
+ },
115
+
116
+ clearAdvanced: function() {
117
+ $('#advanced-options input').val("");
118
+ },
119
+
120
+ // gathers form elements and submits to proper url
121
+ submit: function() {
122
+ $('#'+this.search_form).submit();
123
+ $('#'+this.resultsId).html("Sending new query...");
124
+ },
125
+
126
+ //
127
+ // Misc utitilies
128
+ //
129
+
130
+ autoScroll: function(enabled) {
131
+ if (enabled == true) {
132
+ if (this.scroll_fnId) return; // already running
133
+
134
+ //console.log("scroll ON!")
135
+ window._currPos = 0; // init pos
136
+ this.scroll_fnId = setInterval( function(){
137
+ if (window._currPos < document.height) {
138
+ window.scrollTo(0, document.height);
139
+ window._currPos = document.height;
140
+ }
141
+ }, 100 );
142
+ } else {
143
+ if (!this.scroll_fnId) return;
144
+ //console.log("scroll off")
145
+ if (this.scroll_fnId) {
146
+ clearInterval(this.scroll_fnId);
147
+ window._currPost = 0;
148
+ this.scroll_fnId = null;
149
+ }
150
+ }
151
+ }
152
+
153
+ };
154
+
@@ -0,0 +1,55 @@
1
+ /* ----------------------------------------------------------------------------
2
+ * Log Search Server CSS
3
+ * by John Tajima
4
+ * ----------------------------------------------------------------------------
5
+ */
6
+
7
+ /* 960 reset */
8
+ html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}
9
+ /* 960 text */
10
+ body{font:13px/1.5 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif}a:focus{outline:1px dotted invert}hr{border:0 #ccc solid;border-top-width:1px;clear:both;height:0}h1{font-size:25px}h2{font-size:23px}h3{font-size:21px}h4{font-size:19px}h5{font-size:17px}h6{font-size:15px}ol{list-style:decimal}ul{list-style:disc}li{margin-left:30px}p,dl,hr,h1,h2,h3,h4,h5,h6,ol,ul,pre,table,address,fieldset{margin-bottom:20px}
11
+
12
+ /*
13
+ * Toolbar Styling
14
+ *
15
+ */
16
+
17
+
18
+ #toolbar { color: #333; font-size: 12px; min-width: 980px; height: 130px;
19
+ padding: 0 10px; background: #f0f0f0; border-bottom: 2px solid #000;
20
+ position:fixed; top: 0; margin: 0 -10px; width: 100%;
21
+ font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif;
22
+ }
23
+ #toolbar a { color: blue; background: none; }
24
+ #toolbar a:hover { background: none; }
25
+
26
+ #toolbar #header { height: 20px; padding: 3px 5px; margin: 0 -10px 10px; overflow:hidden; background: #333; border-bottom: 2px solid #222; color: #eee; }
27
+ #toolbar #header h1 { font-size: 16px; line-height: 20px; }
28
+ #toolbar #header h1 span { color: #aaa; }
29
+ #toolbar #header a { text-decoration: none; color: #eee; }
30
+ #toolbar #header a:hover { text-decoration: none; color: #fff;}
31
+ #toolbar .small { font-size: 11px; color: #333; font-weight: bold;}
32
+ #toolbar input.time { width: 20px;}
33
+
34
+ table.actions { width: auto; border-collapse: collapse; }
35
+ table.actions th { font-size: 11px; color: #333; text-align: left; min-width: 100px; padding: 0 5px; }
36
+ table.actions td { padding: 2px 5px; text-align: left; vertical-align: middle;}
37
+ table.actions span.note { font-weight: normal; color: #999; }
38
+ table.actions span.and { font-size: 11px; color: red; font-weight: bold; }
39
+ table.actions span.label { font-weight: bold; cursor:pointer;}
40
+ table.actions input[type=text] { width: 200px; padding: 2px; border: 1px solid #aaa;}
41
+
42
+
43
+ div#option-ctrl { position: fixed; z-index: 100; bottom: 0; right: 0; min-width: 60px; min-height: 20px; height: 20px; padding: 5px; border: 1px solid #ddd; background: #f0f0f0; font-size: 11px; color: #000;
44
+ font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif; }
45
+ div#option-ctrl ul { list-style:none; margin: 0; padding: 0;}
46
+ div#option-ctrl ul li { list-style:none; margin: 0 5px 0 0; float:left; }
47
+
48
+
49
+
50
+ /* output */
51
+ body { margin: 10px; padding-top: 130px; font-family: 'Monaco', 'Deja Vu Sans Mono', 'Inconsolata' ,'Consolas',monospace; background:#111 none repeat scroll 0 0; color:#fff; font-size:10px;}
52
+ a { color: #0f0; }
53
+ a:hover { background-color: #03c; color: white; text-decoration: none; }
54
+
55
+