Error handling and backward compatibility.
[clw.git] / lib / clw / gateway.rb
blob6ca3cc5844a86d6debb021614edd894036732e77
1 # Clarkway tunnelling service
2 # Copyright (C) 2007 Michael Schutte
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the Free
6 # Software Foundation; either version 2 of the License, or (at your option)
7 # any later version.
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
12 # more details.
14 # Modules
15 require "clw/daemon"
17 # External modules
18 require "thread"
19 require "monitor"
20 require "socket"
21 require "openssl"
23 module Clarkway
25     #
26     # Gateways are servers listening for master and client connections,
27     # searching to provide a tunnel between them.
28     #
29     class Gateway < Daemon
31         attr_reader :host, :port, :master
33         #
34         # Create a gateway accepting connections at +host+:+port+, using
35         # +master+ as the master's certificate subject.
36         #
37         def initialize(host, port, master)
38             super()
40             @host = host || "0.0.0.0"
41             @port = port
42             @master = master
43             @online = false
45             @connpipe = IO.pipe
46             @connmap = Hash.new
47             @connmap.extend MonitorMixin
48             @connmap_cond = @connmap.new_cond
49             @connid = 0
50             @connid_lock = Mutex.new
51         end
53         protected
55         #
56         # Wait for masters and clients and handle their connections.
57         #
58         def main
59             begin
60                 tcps = TCPServer.new @host, @port
61             rescue => e
62                 @logger.fatal("Unable to listen on #{@host}:#{@port}.")
63                 return
64             end
65             @logger.info("Waiting for master on #{@host}:#{@port}.")
67             loop do
68                 begin
69                     rawclient = tcps.accept
70                 rescue Interrupt
71                     @logger.info("Terminating gracefully.")
72                     break
73                 rescue => e
74                     @logger.warn("Error while accepting connection: #{e}.")
75                     next
76                 end
78                 register_thread(rawclient) do
79                     begin
80                         client = OpenSSL::SSL::SSLSocket.new(rawclient, context)
81                         client.sync_close = true
82                         client.accept
83                     rescue SSL::SSLError => e
84                         @logger.warn("SSL Error while accepting connection: #{e}.")
85                         client.close
86                         next
87                     end
89                     peerhost = client.peeraddr[3]
90                     peerport = client.peeraddr[1]
92                     if client.peer_cert.subject.to_s == @master
93                         @logger.info("Master connected from " +
94                                      "#{peerhost}:#{peerport}.")
95                         do_master client
97                     elsif not @online
98                         @logger.warn("Client #{client.peer_cert.subject} " +
99                                      "(#{peerhost}:#{peerport}) ignored because " +
100                                      "master is offline.")
101                         client.puts "sorry, master offline"
103                     else
104                         @logger.info("Client #{client.peer_cert.subject} " +
105                                      "connected from #{peerhost}:#{peerport}.")
106                         do_client client
107                     end
109                     client.close
110                 end
111             end
112             
113             stop
114         end
115         
116         #
117         # Stop serving.
118         #
119         def stop
120             @logger.info("Gateway terminated.")
121             super
122         end
124         #
125         # Talk to the master.
126         #
127         def do_master(socket)
128             socket.puts "gateway connection ready"
130             case socket.gets
131             when /^master, /
132                 if @online
133                     socket.puts "already online"
134                     return
135                 end
136                 @logger.info "Master has opened control connection."
137                 @online = true
138                 do_control socket
139                 @online = false
140                 @logger.info "Control connection closed."
142             when /^this is connection (\d+)$/
143                 id = $1.to_i
144                 @logger.info "Master provides connection #{id}."
145                 thread = @connmap[id]
146                 @connmap.synchronize do
147                     @connmap[id] = socket
148                     @connmap_cond.signal
149                 end
150                 thread.join
152             else
153                 @logger.info "Master cancelled connection."
154                 socket.close
155             end
156         end
158         #
159         # Perform control (master - gateway) connection on +socket+.
160         #
161         def do_control(socket)
162             loop do
163                 ios, dummy = select [@connpipe[0], socket]
165                 if ios.include? socket
166                     line = socket.gets
167                     break if line.nil?
168                     case line
169                     when /^(\d+): connection failed$/
170                         @connmap.synchronize do
171                             @connmap[$1.to_i] = :error
172                             @connmap_cond.signal
173                         end
174                     when /^(\d+): in progress$/
175                         @connmap.synchronize do
176                             @connmap[$1.to_i] = :success
177                             @connmap_cond.signal
178                         end
179                     when /^(\d+):/
180                         @connmap.synchronize do
181                             @connmap[$1.to_i] = :error
182                             @connmap_cond.signal
183                         end
184                     end
185                 end
187                 if ios.include? @connpipe[0]
188                     socket.write @connpipe[0].gets
189                 end
190             end
191         end
193         #
194         # Handle a client connection request.
195         #
196         def do_client(socket)
197             peerhost = socket.peeraddr[3]
198             peerport = socket.peeraddr[1]
199             socket.puts "gateway, how can I help?"
201             case line = socket.gets
202             when /^connect me to port (\d+) on (.*)$/
203                 id = nil
204                 # Get a connection id
205                 @connid_lock.synchronize do
206                     id = (@connid += 1)
207                 end
208                 @logger.info("Connection #{id}: " +
209                              "#{peerhost}:#{peerport} <=> " +
210                              "#{$2}:#{$1}")
212                 # Wait for master to open connection
213                 @connmap.synchronize do
214                     @connmap[id] = Thread.current
215                     @connpipe[1].puts "#{id}: #{line.rstrip}"
216                     @connmap_cond.wait_while { @connmap[id] == Thread.current }
217                 end
218                 master = @connmap.delete(id)
220                 if master == :error or master == :success
221                     # Master failed to connect (e.g. no such host)
222                     socket.puts "sorry"
223                 else
224                     # Client may start talking
225                     socket.puts "have fun"
226                     loop do
227                         ios, dummy = select [socket, master]
228                         ios.each do |io|
229                             target = case io
230                                      when socket then   master
231                                      else               socket
232                                      end
233                             begin
234                                 data = io.sysread(BLOCKSIZE)
235                             rescue EOFError
236                                 io.close
237                                 target.close
238                                 break
239                             end
240                             target.write data
241                         end
242                         break if master.closed? or socket.closed?
243                     end
244                 end
246             when /^directly connect me \(([^:]+):(\d+)\) to port (\d+) on (.*)$/
247                 id = nil
248                 @connid_lock.synchronize do
249                     id = (@connid += 1)
250                 end
251                 @logger.info("Connection #{id}: direct, " +
252                              "#{$1}:#{$2} <=> #{$4}:#{$3}")
254                 # Wait for master to be ready
255                 @connmap.synchronize do
256                     @connmap[id] = Thread.current
257                     @connpipe[1].puts("#{id}: directly connect #{$1}:#{$2} " +
258                                       "to port #{$3} on #{$4}; DN is " +
259                                           socket.peer_cert.subject)
260                     @connmap_cond.wait_while { @connmap[id] == Thread.current }
261                 end
262                 success = @connmap.delete(id)
264                 if success == :success
265                     socket.puts "in progress"
266                 else
267                     socket.puts "master failed"
268                 end
269                 socket.close
271             else
272                 socket.puts "sorry"
273                 socket.close
274             end
276             @logger.info("Client #{peerhost}:#{peerport} disconnected.")
277         end
279     end