Fixed --exec together with --listen on 0.0.0.0.
[clw.git] / clw
blobf047a5c4fb8722da824ea0bb641df3909cd1b87b
1 #!/usr/bin/env ruby
2 # Clarkway tunnelling service
3 # Copyright (C) 2007 Michael Schutte
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; either version 2 of the License, or (at your option)
8 # any later version.
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # Modules
16 $: << "lib"
17 require "clw/daemon"
19 # External modules
20 require "logger"
21 require "openssl"
22 require "yaml"
25 # Print command line usage information.
27 def usage
28 puts <<EOF
29 Usage: clw MODE|OPTION ...
30 This is #{Clarkway::ID}, an SSL tunnelling application.
32 Modes:
33 master Connect to a gateway and obey to its connection
34 requests.
35 gateway Accept connections from clients and the master and
36 moderate between them.
37 client Connect to a gateway and get forwarded to the
38 Clarkway master.
39 PROFILE User defined profiles are stored as YAML files in
40 ~/.clw/profiles. See clw(1) for details.
41 FILE Loads user defined profile from FILE. This only
42 works if the file name contains dots or slashes; if
43 it does not, use something like ./FILE.
45 Options:
46 --ca FILE Use FILE as certificate authority; mandatory.
47 --cert FILE Use FILE as certificate; mandatory.
48 --key FILE Use FILE as private key; mandatory.
49 --subject SUBJECT Expect peer to identify with the given certificate
50 subject; mandatory.
51 --gateway HOST[:PORT] Host and port to connect to; mandatory for master
52 and client, ignored for gateway operation.
53 --reconnect N Master: Try to reconnect to the gateway after N
54 seconds [60].
55 --connect HOST:PORT Host and port to connect to; mandatory for client,
56 ignored otherwise.
57 --[no-]stdio Client uses standard input and output for the
58 communication with the target.
59 --listen [HOST:]PORT Gateway: Listen on host HOST [0.0.0.0], port PORT
60 [#{Clarkway::PORT}].
61 Client: Listen on HOST [127.0.0.1], port PORT
62 [chosen randomly].
63 Master: Ignored.
64 --[no-]once Client closes server after first connection.
65 --[no-]daemon (Do not) daemonize after startup. Daemon mode is
66 enabled by default, except for clients acting in
67 stdio mode.
68 --exec COMMAND Client runs COMMAND after opening server (if
69 --listen is active) or talks to it using a pipe (in
70 --stdio mode).
71 --logfile FILE Write logs to FILE. Default is to disable logging if
72 daemon mode is enabled, standard error output
73 otherwise. The special value `-' logs to stderr,
74 `off' forces to disable logging.
75 --loglevel LEVEL Minimum level (info, warn, fatal) of log messages.
76 Default is to log everything (info).
77 --help Show this help.
79 Please report bugs to Michael Schutte <m.schutte.jr@gmail.com>.
80 EOF
81 end
84 # Option parser.
86 class CommandLineParser
88 class Error < RuntimeError; end
90 attr_reader :args, :options
92 CLWHOME = ENV["HOME"] + "/.clw"
94 @@known_options = {
95 "mode" => :string,
96 "ca" => :string,
97 "cert" => :string,
98 "key" => :string,
99 "subject" => :string,
100 "gateway" => :string,
101 "reconnect" => :integer,
102 "connect" => :string,
103 "stdio" => :boolean,
104 "listen" => :string,
105 "once" => :boolean,
106 "daemon" => :boolean,
107 "exec" => :string,
108 "logfile" => :string,
109 "loglevel" => :string,
110 "help" => :boolean,
114 # Create an option parser reading from +args+.
116 def initialize(args)
117 @args = args
118 @options = Hash.new
122 # Process all options.
124 def process
125 loop do
126 arg = @args.shift
127 break if arg.nil?
129 case arg
130 when "master", "gateway", "client"
131 @options["mode"] = arg
132 when /^--(no-?)?(\w+)$/
133 negate = (not $1.nil?)
134 option = $2
135 if @@known_options.include? option
136 type = @@known_options[option]
137 if negate and type != :boolean
138 raise Error, "Tried to assign boolean value to " +
139 "non-boolean option: #{arg}"
140 elsif type == :boolean
141 @options[option] = (not negate)
142 elsif type == :integer
143 if value !~ /^\d+$/
144 raise Error, "Tried to assign string value to " +
145 "integer option: #{arg}"
147 @options[option] = value.to_i
148 elsif type == :string
149 value = @args.shift
150 if value.nil? or value.empty?
151 raise Error, "Option needs a value: #{arg}"
152 else
153 @options[option] = value
156 else
157 raise Error, "Unknown option: #{arg}"
159 when /[.\/]/
160 process_yaml arg
161 else
162 process_yaml CLWHOME + "/profiles/" + arg
166 # Post-process
167 @options.each_pair do |key, value|
168 if value.is_a? String
169 @options[key] = value.gsub(/\{(.*)\}/) {
170 case $1
171 when "open": "{"
172 when "close": "}"
173 when "~": ENV["HOME"] + "/.clw"
174 when "host", "port"
175 if @options["connect"].nil?
177 else
178 @options["connect"].split(":")[
179 ($1 == "host") ? 0 : 1
180 ].to_s
182 else @options[key].to_s
190 # Retrieve an option.
192 def [](option)
193 @options[option]
197 # Set an option manually.
199 def []=(option, value)
200 @options[option] = value
203 protected
206 # Process a YAML file to read options from.
208 def process_yaml(path)
209 begin
210 file = File.new path
211 rescue SystemCallError => e
212 raise Error, "While opening profile: #{e}"
215 begin
216 yaml = YAML::load file
217 rescue => e
218 raise Error, "#{path}: #{e}"
220 unless yaml.is_a? Hash
221 raise Error, "#{path}: Hash expected"
224 yaml.each_pair do |key, value|
225 if @@known_options.include? key
226 type = @@known_options[key]
228 if type == :boolean
229 unless value == true or value == false
230 raise Error, "#{path}: #{key} is a boolean option"
232 @options[key] = value
233 elsif type == :integer
234 unless value.is_a? Fixnum and value >= 0
235 raise Error, "#{path}: #{key} is a numeric option"
237 @options[key] = value
238 elsif type == :string
239 if value.is_a? String and value.empty?
240 raise Error, "#{path}: #{key} is empty"
242 @options[key] = value.to_s
247 file.close
253 # Print an error message.
255 def error(message)
256 STDERR.puts "Error: #{message}"
257 STDERR.puts "Type `clw --help' for details."
261 # The main program.
263 def main
264 options = CommandLineParser.new ARGV
265 begin
266 options.process
267 rescue CommandLineParser::Error => e
268 error "#{e}."
269 return 1
272 if options["help"]
273 usage
274 return 0
277 mandatory = ["ca", "cert", "key", "subject"]
279 case mode = options["mode"]
280 when "master"
281 mandatory += ["gateway"]
282 when "gateway"
283 # No additional mandatory options
284 when "client"
285 mandatory += ["gateway", "connect"]
286 else
287 error "Please specify a valid mode (master, gateway or client)."
288 return 1
291 # Check options
292 mandatory.each do |option|
293 if options[option].nil?
294 error "--#{option} is mandatory for #{mode} operation."
295 return 1
299 # Prepare the daemon
300 subject = options["subject"]
301 case mode
302 when "master"
303 ghost, gport = options["gateway"].split(":")
304 if gport.nil?
305 gport = Clarkway::PORT
306 elsif gport !~ /^\d+$/
307 error "Gateway port has to be numeric."
308 return 1
309 else
310 gport = gport.to_i
313 reconnect = options["reconnect"] || 0
314 reconnect = 60 if reconnect == 0
316 require "clw/master"
317 daemon = Clarkway::Master.new(ghost, gport,
318 options["subject"], reconnect)
320 when "gateway"
321 listen = options["listen"]
322 if listen.nil?
323 lhost, lport = nil, Clarkway::PORT
324 else
325 lhost, lport = listen.split(":")
326 lhost, lport = nil, lhost if lport.nil?
327 if lport !~ /^\d+$/
328 error "Local port has to be numeric."
329 return 1
331 lport = lport.to_i
334 require "clw/gateway"
335 daemon = Clarkway::Gateway.new(lhost, lport, subject)
337 when "client"
338 options["daemon"] = false if options["daemon"].nil?
340 stdio = options["stdio"] || false
341 if stdio
342 lhost = lport = nil
343 else
344 listen = options["listen"] || "0"
345 lhost, lport = listen.split(":")
346 lhost, lport = nil, lhost if lport.nil?
347 if lport !~ /^\d+$/
348 error "Local port has to be numeric."
349 return 1
351 lport = lport.to_i
352 lport = nil if lport == 0
355 chost, cport = options["connect"].split(":")
356 if cport.nil? or cport !~ /^\d+$/
357 error "Target port has to be numeric."
358 return 1
361 ghost, gport = options["gateway"].split(":")
362 if gport.nil?
363 gport = Clarkway::PORT
364 elsif gport !~ /^\d+$/
365 error "Gateway port has to be numeric."
366 return 1
367 else
368 gport = gport.to_i
371 command = options["exec"]
372 unless command.nil?
373 options["once"] = true
374 options["daemon"] = false
377 require "clw/client"
378 daemon = Clarkway::Client.new(chost, cport, ghost, gport, subject)
379 daemon.once = options["once"] || false
380 daemon.stdio = stdio
381 daemon.exec = command
382 daemon.localhost = lhost unless lhost.nil?
383 daemon.localport = lport
384 options["daemon"] = false if stdio
387 # SSL initialization
388 daemon.ca = options["ca"]
389 begin
390 daemon.cert = OpenSSL::X509::Certificate.new File::read(options["cert"])
391 daemon.key = OpenSSL::PKey::RSA.new File::read(options["key"])
392 rescue => e
393 error "While initializing OpenSSL: #{e}."
394 return 1
397 # Logging
398 daemonize = options["daemon"]
399 daemonize = true if daemonize.nil?
400 logfile = options["logfile"]
401 loglevel = options["loglevel"]
403 if logfile.nil?
404 logfile = daemonize ? nil : STDERR
405 elsif logfile == "-"
406 logfile = STDERR
407 elsif logfile == "off"
408 logfile = nil
409 else
410 begin
411 logfile = File.new(logfile,
412 File::WRONLY | File::APPEND | File::CREAT)
413 logfile.sync = true
414 rescue SystemCallError => e
415 error "While opening logfile: #{e}."
416 return 1
419 daemon.logger = Logger.new logfile
421 case loglevel
422 when "fatal"
423 daemon.logger.sev_threshold = Logger::FATAL
424 when "warn"
425 daemon.logger.sev_threshold = Logger::WARN
426 else
427 daemon.logger.sev_threshold = Logger::INFO
430 # Start the daemon
431 daemon.start daemonize
433 # Exit successfully
437 exit main if $0 == __FILE__
439 # vim:tw=78:fmr=<<<,>>>:fdm=marker: