1 from twisted
.web
import resource
, server
, static
, error
as http_error
2 from twisted
.names
import client
3 from twisted
.internet
import reactor
, error
4 from authgateengine
import login_optional
, getSessionData
5 import md5
, sys
, os
, time
, config
, qwebirc
.config_options
as config_options
, traceback
, socket
6 import qwebirc
.ircclient
as ircclient
7 from adminengine
import AdminEngineAction
8 from qwebirc
.util
import HitCounter
9 import qwebirc
.dns
as qdns
10 import qwebirc
.util
.qjson
as json
14 return md5
.md5(os
.urandom(16)).hexdigest()
16 class BufferOverflowException(Exception):
19 class AJAXException(Exception):
22 class IDGenerationException(Exception):
25 class PassthruException(Exception):
29 EMPTY_JSON_LIST
= json
.dumps([])
32 def decorator(*args
, **kwargs
):
34 x
= fn(*args
, **kwargs
)
36 return server
.NOT_DONE_YET
38 except AJAXException
, e
:
40 except PassthruException
, e
:
46 def cleanupSession(id):
53 def __init__(self
, id):
55 self
.subscriptions
= []
61 self
.cleanupschedule
= None
63 def subscribe(self
, channel
, notifier
):
64 timeout_entry
= reactor
.callLater(config
.HTTP_AJAX_REQUEST_TIMEOUT
, self
.timeout
, channel
)
65 def cancel_timeout(result
):
66 if channel
in self
.subscriptions
:
67 self
.subscriptions
.remove(channel
)
69 timeout_entry
.cancel()
70 except error
.AlreadyCalled
:
72 notifier
.addCallbacks(cancel_timeout
, cancel_timeout
)
74 if len(self
.subscriptions
) >= config
.MAXSUBSCRIPTIONS
:
75 self
.subscriptions
.pop(0).close()
77 self
.subscriptions
.append(channel
)
80 def timeout(self
, channel
):
84 channel
.write(EMPTY_JSON_LIST
)
85 if channel
in self
.subscriptions
:
86 self
.subscriptions
.remove(channel
)
88 def flush(self
, scheduled
=False):
92 if not self
.buffer or not self
.subscriptions
:
99 self
.schedule
= reactor
.callLater(self
.throttle
- t
, self
.flush
, True)
102 # process the rest of the packet
104 if not self
.schedule
:
105 self
.schedule
= reactor
.callLater(0, self
.flush
, True)
108 self
.throttle
= t
+ config
.UPDATE_FREQ
110 encdata
= json
.dumps(self
.buffer)
115 for x
in self
.subscriptions
:
119 self
.subscriptions
= newsubs
120 if self
.closed
and not self
.subscriptions
:
121 cleanupSession(self
.id)
123 def event(self
, data
):
124 newbuflen
= self
.buflen
+ len(data
)
125 if newbuflen
> config
.MAXBUFLEN
:
127 self
.client
.error("Buffer overflow.")
130 self
.buffer.append(data
)
131 self
.buflen
= newbuflen
134 def push(self
, data
):
136 self
.client
.write(data
)
138 def disconnect(self
):
139 # keep the session hanging around for a few seconds so the
140 # client has a chance to see what the issue was
143 reactor
.callLater(5, cleanupSession
, self
.id)
145 # DANGER! Breach of encapsulation!
146 def connect_notice(line
):
147 return "c", "NOTICE", "", ("AUTH", "*** (qwebirc) %s" % line
)
150 def __init__(self
, request
):
151 self
.request
= request
153 class SingleUseChannel(Channel
):
154 def write(self
, data
):
155 self
.request
.write(data
)
156 self
.request
.finish()
160 self
.request
.finish()
162 class MultipleUseChannel(Channel
):
163 def write(self
, data
):
164 self
.request
.write(data
)
167 class AJAXEngine(resource
.Resource
):
170 def __init__(self
, prefix
):
172 self
.__connect
_hit
= HitCounter()
173 self
.__total
_hit
= HitCounter()
176 def render_POST(self
, request
):
177 path
= request
.path
[len(self
.prefix
):]
179 handler
= self
.COMMANDS
.get(path
[1:])
180 if handler
is not None:
181 return handler(self
, request
)
183 raise PassthruException
, http_error
.NoResource().render(request
)
185 def newConnection(self
, request
):
186 ticket
= login_optional(request
)
188 ip
= request
.getClientIP()
190 nick
= request
.args
.get("nick")
192 raise AJAXException
, "Nickname not supplied."
193 nick
= ircclient
.irc_decode(nick
[0])
195 password
= request
.args
.get("password")
196 if password
is not None:
197 password
= ircclient
.irc_decode(password
[0])
200 id = get_session_id()
201 if not Sessions
.get(id):
204 raise IDGenerationException()
206 session
= IRCSession(id)
208 qticket
= getSessionData(request
).get("qticket")
212 service_mask
= config
.AUTH_SERVICE
213 msg_mask
= service_mask
.split("!")[0] + "@" + service_mask
.split("@", 1)[1]
214 perform
= ["PRIVMSG %s :TICKETAUTH %s" % (msg_mask
, qticket
)]
216 ident
, realname
= config
.IDENT
, config
.REALNAME
217 if ident
is config_options
.IDENT_HEX
or ident
is None: # latter is legacy
218 ident
= socket
.inet_aton(ip
).encode("hex")
219 elif ident
is config_options
.IDENT_NICKNAME
:
224 def proceed(hostname
):
225 kwargs
= dict(nick
=nick
, ident
=ident
, ip
=ip
, realname
=realname
, perform
=perform
, hostname
=hostname
)
226 if password
is not None:
227 kwargs
["password"] = password
229 client
= ircclient
.createIRC(session
, **kwargs
)
230 session
.client
= client
232 if not hasattr(config
, "WEBIRC_MODE") or config
.WEBIRC_MODE
== "hmac":
234 elif config
.WEBIRC_MODE
!= "hmac":
235 notice
= lambda x
: session
.event(connect_notice(x
))
236 notice("Looking up your hostname...")
237 def callback(hostname
):
238 notice("Found your hostname.")
240 def errback(failure
):
241 notice("Couldn't look up your hostname!")
243 qdns
.lookupAndVerifyPTR(ip
, timeout
=[config
.DNS_TIMEOUT
]).addCallbacks(callback
, errback
)
245 Sessions
[id] = session
249 def getSession(self
, request
):
250 bad_session_message
= "Invalid session, this most likely means the server has restarted; close this dialog and then try refreshing the page."
252 sessionid
= request
.args
.get("s")
253 if sessionid
is None:
254 raise AJAXException
, bad_session_message
256 session
= Sessions
.get(sessionid
[0])
258 raise AJAXException
, bad_session_message
261 def subscribe(self
, request
):
262 request
.channel
.cancelTimeout()
263 self
.getSession(request
).subscribe(SingleUseChannel(request
), request
.notifyFinish())
266 def push(self
, request
):
267 command
= request
.args
.get("c")
269 raise AJAXException
, "No command specified."
272 decoded
= ircclient
.irc_decode(command
[0])
274 session
= self
.getSession(request
)
276 if len(decoded
) > config
.MAXLINELEN
:
278 raise AJAXException
, "Line too long."
281 session
.push(decoded
)
282 except AttributeError: # occurs when we haven't noticed an error
284 raise AJAXException
, "Connection closed by server; try reconnecting by reloading the page."
285 except Exception, e
: # catch all
287 traceback
.print_exc(file=sys
.stderr
)
288 raise AJAXException
, "Unknown error."
292 def closeById(self
, k
):
296 s
.client
.client
.error("Closed by admin interface")
299 def adminEngine(self
):
301 "Sessions": [(str(v
.client
.client
), AdminEngineAction("close", self
.closeById
, k
)) for k
, v
in Sessions
.iteritems() if not v
.closed
],
302 "Connections": [(self
.__connect
_hit
,)],
303 "Total hits": [(self
.__total
_hit
,)],
306 COMMANDS
= dict(p
=push
, n
=newConnection
, s
=subscribe
)