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)
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
25 # Print command line usage information.
29 Usage: clw MODE|OPTION ...
30 This is #{Clarkway::ID}, an SSL tunnelling application.
33 master Connect to a gateway and obey to its connection
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
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.
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
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
55 --connect HOST:PORT Host and port to connect to; mandatory for client,
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
61 Client: Listen on HOST [127.0.0.1], port PORT
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
68 --exec COMMAND Client runs COMMAND after opening server (if
69 --listen is active) or talks to it using a pipe (in
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>.
86 class CommandLineParser
88 class Error
< RuntimeError
; end
90 attr_reader
:args, :options
92 CLWHOME
= ENV["HOME"] + "/.clw"
100 "gateway" => :string,
101 "reconnect" => :integer,
102 "connect" => :string,
106 "daemon" => :boolean,
108 "logfile" => :string,
109 "loglevel" => :string,
114 # Create an option parser reading from +args+.
122 # Process all options.
130 when "master", "gateway", "client"
131 @options["mode"] = arg
132 when /^--(no-?)?(\w+)$/
133 negate
= (not $1.nil?)
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
144 raise Error
, "Tried to assign string value to " +
145 "integer option: #{arg}"
147 @options[option
] = value
.to_i
148 elsif type
== :string
150 if value
.nil? or value
.empty
?
151 raise Error
, "Option needs a value: #{arg}"
153 @options[option
] = value
157 raise Error
, "Unknown option: #{arg}"
162 process_yaml CLWHOME
+ "/profiles/" + arg
167 @options.each_pair
do |key
, value
|
168 if value
.is_a
? String
169 @options[key
] = value
.gsub(/\{(.*)\}/) {
173 when "~": ENV["HOME"] + "/.clw"
175 if @options["connect"].nil?
178 @options["connect"].split(":")[
179 ($1 == "host") ? 0 : 1
182 else @options[key
].to_s
190 # Retrieve an option.
197 # Set an option manually.
199 def []=(option
, value
)
200 @options[option
] = value
206 # Process a YAML file to read options from.
208 def process_yaml(path
)
211 rescue SystemCallError
=> e
212 raise Error
, "While opening profile: #{e}"
216 yaml
= YAML
::load file
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
]
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
253 # Print an error message.
256 STDERR.puts
"Error: #{message}"
257 STDERR.puts
"Type `clw --help' for details."
264 options
= CommandLineParser
.new
ARGV
267 rescue CommandLineParser
::Error => e
277 mandatory
= ["ca", "cert", "key", "subject"]
279 case mode
= options
["mode"]
281 mandatory
+= ["gateway"]
283 # No additional mandatory options
285 mandatory
+= ["gateway", "connect"]
287 error
"Please specify a valid mode (master, gateway or client)."
292 mandatory
.each
do |option
|
293 if options
[option
].nil?
294 error
"--#{option} is mandatory for #{mode} operation."
300 subject
= options
["subject"]
303 ghost
, gport
= options
["gateway"].split(":")
305 gport
= Clarkway
::PORT
306 elsif gport
!~
/^\d+$/
307 error
"Gateway port has to be numeric."
313 reconnect
= options
["reconnect"] || 0
314 reconnect
= 60 if reconnect
== 0
317 daemon
= Clarkway
::Master.new(ghost
, gport
,
318 options
["subject"], reconnect
)
321 listen
= options
["listen"]
323 lhost
, lport
= nil, Clarkway
::PORT
325 lhost
, lport
= listen
.split(":")
326 lhost
, lport
= nil, lhost
if lport
.nil?
328 error
"Local port has to be numeric."
334 require "clw/gateway"
335 daemon
= Clarkway
::Gateway.new(lhost
, lport
, subject
)
338 options
["daemon"] = false if options
["daemon"].nil?
340 stdio
= options
["stdio"] || false
344 listen
= options
["listen"] || "0"
345 lhost
, lport
= listen
.split(":")
346 lhost
, lport
= nil, lhost
if lport
.nil?
348 error
"Local port has to be numeric."
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."
361 ghost
, gport
= options
["gateway"].split(":")
363 gport
= Clarkway
::PORT
364 elsif gport
!~
/^\d+$/
365 error
"Gateway port has to be numeric."
371 command
= options
["exec"]
373 options
["once"] = true
374 options
["daemon"] = false
378 daemon
= Clarkway
::Client.new(chost
, cport
, ghost
, gport
, subject
)
379 daemon
.once
= options
["once"] || false
381 daemon
.exec
= command
382 daemon
.localhost
= lhost
unless lhost
.nil?
383 daemon
.localport
= lport
384 options
["daemon"] = false if stdio
388 daemon
.ca
= options
["ca"]
390 daemon
.cert
= OpenSSL
::X509::Certificate.new File
::read(options
["cert"])
391 daemon
.key
= OpenSSL
::PKey::RSA.new File
::read(options
["key"])
393 error
"While initializing OpenSSL: #{e}."
398 daemonize
= options
["daemon"]
399 daemonize
= true if daemonize
.nil?
400 logfile
= options
["logfile"]
401 loglevel
= options
["loglevel"]
404 logfile
= daemonize
? nil : STDERR
407 elsif logfile
== "off"
411 logfile
= File
.new(logfile
,
412 File
::WRONLY | File
::APPEND | File
::CREAT)
414 rescue SystemCallError
=> e
415 error
"While opening logfile: #{e}."
419 daemon
.logger
= Logger
.new logfile
423 daemon
.logger
.sev_threshold
= Logger
::FATAL
425 daemon
.logger
.sev_threshold
= Logger
::WARN
427 daemon
.logger
.sev_threshold
= Logger
::INFO
431 daemon
.start daemonize
437 exit main
if $0 == __FILE__
439 # vim:tw=78:fmr=<<<,>>>:fdm=marker: