added some notes
[k8lst.git] / samples / classbrowser.st
blob61977b4d718f43ed70f0d44ad8c008952efeb596
2   simple HTTP server and class browser
3   Copyright (C) 199? <unknown author>
4   Changes by Ketmar // Vampire Avalon
6   This program is free software; you can redistribute it and/or modify
7   it under the terms of the GNU General Public License as published by
8   the Free Software Foundation; either version 2 of the License, or
9   (at your option) any later version.
11   This program is distributed in the hope that it will be useful,
12   but WITHOUT ANY WARRANTY; without even the implied warranty of
13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   GNU General Public License for more details.
16   You should have received a copy of the GNU General Public License
17   along with this program; if not, write to the Free Software
18   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 Requires [
21   socket
25 "******************************************************
26    String class additions.  These help parsing and
27    handling URLs and HTML.
28  ******************************************************"
29 String extend [
30 htmlEscape [
31   "encode '<>&'"
32   | res |
33   (self includes: '<>&') ifFalse: [ ^self ].
34   res := StringBuffer new.
35   self do: [:c |
36     c == $< ifTrue: [ res << '&lt;' ]
37     ifFalse: [
38       c == $> ifTrue: [ res << '&gt;' ]
39       ifFalse: [
40         c == $& ifTrue: [ res << '&amp;' ]
41         ifFalse: [ res << c ]]]].
42   ^res asString
47 "****************************************"
48 "           HTTPRequest class            "
49 "****************************************"
50 Object subclass: HTTPRequest [
51 | sock reqPath reqAction reqArgs reqRawData reqPathAndArgs reqError reqLength |
53 read: aSock [
54   sock := aSock.
55    ^(self rawData size > 0)
58 response: aResp [
59   | responseSize tmpResponse |
60   tmpResponse := StringBuffer new.
61   "get the response size"
62   responseSize := aResp size.
63   "make HTTP headers"
64   tmpResponse <<
65     'HTTP/1.0 200 LittleSmalltalk\r\n' <<
66     'Content-Type: text/html\r\n' <<
67     ('Content-Length: ' + (responseSize printString) + '\r\n') <<
68     '\r\n'.
69   "add the response text"
70   tmpResponse << aResp.
71   sock send: tmpResponse asString.
72   "close the connection now"
73   sock close.
74   ^self
77 rawData [
78   | i tempData contentLength |
79   "read the request raw data; this does some parsing"
80   reqRawData isNil ifFalse: [ ^reqRawData ].
81   "is the socket nil? or not open?"
82   sock isNil ifTrue: [ ^nil ].
83   " get the data from the socket until we see the header/body delimiter"
84   tempData := sock recv: 1.
85   [ (tempData position: '\r\n\r\n') isNil]
86     whileTrue: [ tempData := tempData + (sock recv: 1) ].
87   "OK, we have all the headers, what kind of request is it?"
88   reqRawData := tempData.
89   reqLength := tempData size.
90   "if this is a POST, we need to get the length and read the data"
91   ((self action) = 'POST') ifTrue: [
92     'Processing POST action.' printNl.
93     i := tempData position: 'Content-Length:' .
94     i isNil ifTrue: [ reqError := '400 POST without Content-Length header'. ^nil ].
95     "find the first digit character"
96     i := i + ('Content-Length:' size).
97     [ (tempData at: i) isBlank ] whileTrue: [ i := i+1 ].
98     contentLength := 0.
99     "convert the size into an integer while reading it in"
100     [ (tempData at: i) isDigit ] whileTrue:
101       [ contentLength := (contentLength * 10) + (((tempData at: i) value) - ($0 value)). i := i+1 ].
102     "store the length for later"
103     reqLength := contentLength.
104     "the total length is the length of the header plus separator plus body, -1 for zero start"
105     contentLength := contentLength + (tempData position: '\r\n\r\n') + 3.
106     "read until we have all the data"
107     [ (tempData size) < contentLength ] whileTrue: [tempData := tempData + (sock recv: 1)].
108    ]
109    ifFalse: [ reqLength := 0 ].
110   "we have all the raw data. We've set reqAction, reqLength already, so set reqRawData"
111   reqRawData := tempData.
112   ^reqRawData.
115 pathAndArgs [
116   | lines firstLine fields |
117   reqPathAndArgs isNil ifFalse: [ ^reqPathAndArgs ].
118   "break raw data into lines"
119   lines := (self rawData) break: '\r\n'.
120   firstLine := lines first.
121   "break on spaces"
122   fields := firstLine break: ' '.
123   "path plus arguments is second field"
124   fields removeFirst.
125   reqPathAndArgs := fields first.
126   ^reqPathAndArgs.
129 action [
130   "if it was set once, return it"
131   reqAction isNil ifFalse: [ ^reqAction.].
132   'reqAction before compileWithClass: ' print.
133   reqAction printString printNl.
134   ((self rawData) position: 'GET') = 1 ifTrue: [ reqAction := 'GET'. ].
135   ((self rawData) position: 'POST') = 1 ifTrue: [ reqAction := 'POST'. ].
136   reqAction isNil ifTrue: [ reqAction := 'UNKNOWN' ].
137   'reqAction: ' print.
138   reqAction printString printNl.
139   ^reqAction.
142 path [
143   | i pathArgField |
144   reqPath isNil ifFalse: [ ^reqPath ].
145   reqPath = '' ifTrue: [ ^nil ].
146   pathArgField := self pathAndArgs.
147   pathArgField isNil ifTrue: [ reqPath := ''. ^nil ].
148   i := pathArgField position: '?'.
149   i isNil ifTrue: [ reqPath := pathArgField. ^reqPath ].
150   reqPath := pathArgField from: 1 to: (i - 1).
151   ^reqPath.
154 args [
155   | i pathArgField argsData keyValList key val argList |
156   "get args for both URL and POST data"
157   reqArgs isNil ifFalse: [ ^reqArgs ].
158   reqArgs := Dictionary new.
159   "concatenate args"
160   pathArgField := self pathAndArgs.
161   (pathArgField isNil) ifFalse: [
162     i := pathArgField position: '?'.
163     i isNil ifFalse: [
164       "copy the data"
165       argsData := pathArgField from: (i+1) to: (pathArgField size).
166       "append a & to make sure that we break correctly"
167       argsData := argsData + '&'.
168     ]
169   ].
170   "copy data from the form data if this is a POST"
171   (self action) = 'POST' ifTrue: [
172     i := ((self rawData size) + 1) - reqLength.
173     argsData := argsData + ((self rawData) from: i to: (self rawData size))
174   ].
175   "do a little error checking"
176   argsData isNil ifTrue: [ ^reqArgs ].
177   (argsData size) = 0 ifTrue: [ ^reqArgs ].
178   "split up the key value pairs"
179   keyValList := argsData break: '&'.
180   keyValList do: [ :keyValField |
181     argList := keyValField break: '='.
182     key := argList first.
183     argList removeFirst.
184     " handle case where key indicates a flag "
185     (argList size) = 0
186       ifTrue: [ val := true asString ]
187       ifFalse: [ val := argList first asString ].
188     val isNil ifTrue: [ val := 'no value' ].
189     reqArgs at: (key fromUrl asSymbol) put: (val fromUrl).
190   ].
191   ^reqArgs.
194 at: aSymbol [
195   ^(self args) at: aSymbol ifAbsent: [ nil ].
200 "*******************************"
201 "     HTTPDispatcher class      "
202 "*******************************"
203 Object subclass: HTTPDispatcher [
204 | map env runFlag sock request errorHandler |
206 register: aBlock at: aPath [
207   map isNil ifTrue: [ map := Dictionary new ].
208   map at: aPath put: aBlock.
209   ^self.
212 registerErrorHandler: anObj [
213   errorHandler := anObj.
214   ^self.
217 startOn: aSock [
218   | tmpRequest aBlock clientSock |
219   runFlag := true.
220   env := Dictionary new.
221   aSock listen < 0 ifTrue: [
222     aSock close.
223     self error: 'can''t listen'
224   ].
225   [ runFlag = true ] whileTrue: [
226     "get a request from the socket and dispatch it"
227     clientSock := aSock accept.
228     tmpRequest := HTTPRequest new.
229     (tmpRequest read: clientSock) ifTrue: [
230       aBlock := map at: (tmpRequest path) ifAbsent: [ nil ].
231       ( aBlock isNil )
232         ifTrue: [ errorHandler value: tmpRequest value: env]
233         ifFalse: [ aBlock value: tmpRequest value: env ].
234     ].
235     clientSock close.
236   ].
239 stop [
240   runFlag := false.
245 "*********************************"
246 "     HTTPClassBrowser class     "
247 "*********************************"
248 Object subclass: HTTPClassBrowser [
250 listPackagesOn: aReq [
251   | outBuf |
252   outBuf := StringBuffer new.
253   outBuf << '<HTML><BODY>'.
254   Package packages keysDo: [ :obj |
255     obj := obj asString.
256     outBuf <<
257       '<A HREF="/class_list_frame?package=' <<
258       (obj toUrl) <<
259       '" target="class_list_frame">' <<
260       obj <<
261       '</A><BR>'
262   ].
263   outBuf << '</BODY></HTML>'.
264   ^aReq response: outBuf
267 addClassName: outBuf class: obj [
268   | name |
269   obj isMeta ifFalse: [
270     name := obj printString.
271     outBuf <<
272       '<A HREF="/method_list_frame?class=' <<
273       (name toUrl) <<
274       '" target="method_list_frame">' <<
275       name <<
276       '</A><BR>'
277   ].
280 listClassesOn: aReq [
281   | outBuf pkg |
282   pkg := aReq at: #package.
283   pkg ifNotNil: [ pkg := Package find: pkg ].
284   outBuf := StringBuffer new.
285   outBuf << '<HTML><BODY>'.
286   pkg
287     ifNil: [
288       globals do: [:obj | (obj isKindOf: Class) ifTrue: [ self addClassName: outBuf class: obj ]].
289     ] ifNotNil: [
290       pkg classes do: [:obj | (obj isKindOf: Class) ifTrue: [ self addClassName: outBuf class: obj ]].
291     ].
292   outBuf << '</BODY></HTML>'.
293   ^aReq response: outBuf
296 addMethodToBuf: outBuf class: classStr name: name asMeta: asMeta [
297   | n |
298   n := name asString.
299   asMeta ifTrue: [ n := '^' + n ].
300   outBuf <<
301     '<A HREF="/edit_frame?class=' <<
302     classStr <<
303     '&method=' <<
304     (n toUrl) <<
305     '" target="edit_frame">' <<
306     (n htmlEscape) <<
307     '</A><BR>'.
310 addClassMethodsToBuf: outBuf class: class classStr: classStr asMeta: asMeta [
311   (class methods size) = 0 ifTrue: [ ^false ].
312   class methods keysAndValuesDo: [ :name :meth |
313     "HTML doesn't like < signs"
314     self addMethodToBuf: outBuf class: classStr name: name asMeta: asMeta.
315   ].
316   ^true
319 listMethodsOn: aReq [
320   | outBuf classStr class cc |
321   outBuf := StringBuffer new.
322   "header for page"
323   outBuf << '<HTML><BODY>'.
324   classStr := aReq at: #class.
325   "if there isn't a class string chosen"
326   classStr isNil ifTrue: [ outBuf << '<B>No class chosen.</B></BODY></HTML>'. ^aReq response: outBuf ].
327   class := globals at: (classStr asSymbol) ifAbsent: [ nil ].
328   class isNil ifTrue: [ outBuf << '<B>No such class!</B></BODY></HTML>'. ^aReq response: outBuf ].
329   "some classes have no methods"
330   (cc := class class) isMeta ifTrue: [
331     self addClassMethodsToBuf: outBuf class: cc classStr: classStr asMeta: true.
332     (cc methods size) = 0 ifFalse: [
333       (class methods size) = 0 ifFalse: [ outBuf << '<hr>' ]
334     ].
335   ].
336   self addClassMethodsToBuf: outBuf class: class classStr: classStr asMeta: false.
337   "outBuf << '<B>No methods in class</B>'"
338   outBuf << '</BODY></HTML>'.
339   ^aReq response: outBuf.
342 frameSubclass: aReq [
343   | outBuf classStr newClassStr |
344   outBuf := StringBuffer new.
345   outBuf << '<html><body>'.
346   aReq action = 'POST' ifFalse: [
347     outBuf << '<b>POST form submission required.</b>'
348   ] ifTrue: [
349     classStr := aReq at: #class.
350     newClassStr := aReq at: #subclassname.
351     classStr printNl.
352     newClassStr printNl.
353     (globals at: (classStr asSymbol) ifAbsent: [ nil ]) ifNil: [
354       outBuf << '<b>ERROR: can''t subclass non-existing class.</b>'
355     ] ifNotNil: [
356       aReq at: #class put: newClassStr.
357       ^self listMethodsOn: aReq.
358     ].
359   ].
360   outBuf << '</body></html>'.
361   ^aReq response: outBuf.
364 editMethodOn: aReq [
365   | outBuf classStr class methStr method body |
366   outBuf := StringBuffer new.
367   outBuf << '<HTML><BODY>'.
368   classStr := aReq at: #class.
369   "if there isn't a class string chosen"
370   classStr isNil ifTrue: [ outBuf << '<B>No class chosen.</B></BODY></HTML>'. ^aReq response: outBuf ].
371   class := globals at: (classStr asSymbol) ifAbsent: [ nil ].
372   class isNil ifTrue: [ outBuf << '<B>No such class!</B></BODY></HTML>'. ^aReq response: outBuf ].
373   "if there isn't a method string chosen"
374   methStr := aReq at: #method.
375   methStr printNl.
376   "debugging (aReq args) keysAndValuesDo: [ :key :val | outBuf << ((key printString) + ' = ' + (val printString) + '<BR>') ]."
377   methStr = 'no value' ifTrue: [ outBuf + '<B>No Value!</B></BODY></HTML>'. ^aReq response: outBuf ].
378   ((methStr isNil) or: [ methStr isEmpty ]) ifTrue: [ outBuf << '<B>No method chosen.</B></BODY></HTML>'. ^aReq response: outBuf ].
379   methStr firstChar == $^
380     ifTrue: [
381       method := (class class methods) at: ((methStr from: 2) asSymbol) ifAbsent: [ nil ].
382     ] ifFalse: [
383       method := (class methods) at: (methStr asSymbol) ifAbsent: [ nil ].
384     ].
385   method isNil ifTrue: [ outBuf << '<B>No such method!</B></BODY></HTML>'. ^aReq response: outBuf ].
386   (body := method text) ifNil: [ body := '"no source"' ].
387   methStr firstChar == $^ ifTrue: [ body := '^' + body ].
388   outBuf <<
389     '<FORM ACTION="/compile_method?class=' <<
390     classStr <<
391     '&method=' <<
392     (method name printString toUrl) <<
393     '" ENCTYPE="application/x-www-form-urlencoded" METHOD="POST">' <<
394     '<TEXTAREA COLS=60 ROWS=40 NAME="methsrc" WRAP="OFF">' <<
395     (body htmlEscape) <<
396     '</TEXTAREA>' <<
397     '<BR><INPUT TYPE=SUBMIT NAME=compile VALUE="Compile">' <<
398     '<br><br><input type="submit" name=dogst value="Execute GST-style code">' <<
399      '</FORM>' <<
400      '</BODY></HTML>'.
401   ^aReq response: outBuf.
404 compileMethodOn: aReq [
405   | outBuf classStr class methSrc action meth p warnings |
406   outBuf := StringBuffer new.
407   outBuf << '<HTML><BODY>'.
408   "check to make sure this is a POST"
409   action := aReq action.
410   action = 'POST' ifFalse: [
411     outBuf << '<B>POST form submission required.</B></BODY></HTML> '.
412     ^aReq response: outBuf.
413   ].
414   methSrc := aReq at: #methsrc.
415   (aReq at: #compile) ~= 'Compile' ifTrue: [
416     methSrc isNil ifTrue: [ outBuf << '<B>No source!</B></BODY></HTML>'. ^aReq response: outBuf ].
417     "filter out carriage returns"
418     methSrc := (methSrc printString) reject: [ :c | c isCR ].
419     methSrc := StringStream newWith: (methSrc removeTrailingBlanks).
420     warnings := ''.
421     p := GSTParser newWith: methSrc.
422     p errorBlock: [ :msg |
423       outBuf << '<b>COMPILATION ERROR: ' << (msg htmlEscape) << '</b>'.
424       outBuf << '</body></html>'.
425       ^aReq response: outBuf.
426     ].
427     p warningBlock: [ :msg |
428       outBuf << '<b>COMPILATION WARNING: ' << (msg htmlEscape) << '</b><br />'.
429     ].
430     p parse.
431     outBuf << warnings << '</body></html>'.
432     ^aReq response: outBuf.
433   ].
434   "if there isn't a class string chosen"
435   classStr := aReq at: #class.
436   classStr isNil ifTrue: [ outBuf << '<B>No class chosen.</B></BODY></HTML>'. ^aReq response: outBuf ].
437   class := globals at: (classStr asSymbol) ifAbsent: [ nil ].
438   class isNil ifTrue: [ outBuf << '<B>No such class!</B></BODY></HTML>'. ^aReq response: outBuf ].
439   "get the method source."
440   methSrc isNil ifTrue: [ outBuf << '<B>No method source!</B></BODY></HTML>'. ^aReq response: outBuf ].
441   methSrc isEmpty ifTrue: [ outBuf << '<B>No method source!</B></BODY></HTML>'. ^aReq response: outBuf ].
442   "compile and add method"
443   p := LstCompiler new.
444   p errorBlock: [ :msg :lineNum |
445     outBuf << '<b>COMPILATION ERROR: near line ' << (lineNum asString) << ': ' << (msg htmlEscape) << '</b>'.
446     outBuf << '</body></html>'.
447     ^aReq response: outBuf.
448   ].
449   p warningBlock: [ :msg :lineNum |
450     outBuf << '<b>COMPILATION WARNING: near line' << (lineNum asString) << ': ' << (msg htmlEscape) << '</b><br />'.
451   ].
452   (meth := (class addMethod: methSrc withCompiler: p)) ifNotNil: [
453     outBuf << (meth name printString) << ' added.'.
454   ].
455   outBuf << '</body></html>'.
456   ^aReq response: outBuf.
459 showBaseFrameOn: aReq [
460   | outBuf |
461   outBuf := StringBuffer new.
463   outBuf <<
464     '<HTML><FRAMESET COLS="50%,50%" FRAMEBORDER="YES">' <<
465     '<FRAME SRC="/control_list_frame" NAME="control_list_frame">' <<
466     '<FRAME SRC="/edit_frame" NAME="edit_frame">' <<
467     '</FRAMESET></HTML>'.
469   ^aReq response: outBuf.
472 showControlListFrameOn: aReq [
473   | outBuf |
474   outBuf := StringBuffer new.
476   outBuf << '<HTML><FRAMESET ROWS="80%,20%" FRAMEBORDER="YES">' <<
477     '<FRAME SRC="/list_frame" NAME="list_frame">' <<
478     '<FRAME SRC="/control_frame" NAME="control_frame">' <<
479     '</FRAMESET></HTML>'.
481   ^aReq response: outBuf.
484 showListFrameOn: aReq [
485   | outBuf |
486   outBuf := StringBuffer new.
488   outBuf << '<HTML><FRAMESET COLS="20%,40%,40%" FRAMEBORDER="YES">' <<
489     '<FRAME SRC="/package_list_frame" NAME="package_list_frame">' <<
490     '<FRAME SRC="/class_list_frame" NAME="class_list_frame">' <<
491     '<FRAME SRC="/method_list_frame" NAME="method_list_frame">' <<
492     '</FRAMESET></HTML>'.
494   ^aReq response: outBuf.
497 showControlFrameOn: aReq [
498   | outBuf |
499   outBuf := StringBuffer new.
501   outBuf addLast: '<HTML><BODY><FORM METHOD="GET" ACTION="/stop" TARGET="_top">'.
502   outBuf addLast: '<INPUT TYPE=SUBMIT NAME=stop VALUE="Stop Browser">'.
503   outBuf addLast: '</FORM></BODY></HTML>'.
505   ^aReq response: outBuf.
508 showErrorOn: aReq [
509   | outBuf |
510   outBuf := StringBuffer new.
512   outBuf addLast: '<HTML><BODY><B>Path not recognized!</B><BR>'.
513   outBuf addLast: '<PRE>'.
514   outBuf addLast: ('path: ' + (aReq path) + '\n').
515   aReq args isNil ifFalse: [ (aReq args) keysAndValuesDo:
516           [ :key :val | outBuf addLast: ((key printString) + '=' + (val printString) + '\n') ] ].
518   outBuf addLast: '</PRE></BODY></HTML>'.
520   ^aReq response: outBuf.
523 startOn: aSock [
524   | dispatcher |
525   dispatcher := HTTPDispatcher new.
527   dispatcher register: [:aReq :anEnv | self showBaseFrameOn: aReq. nil]
528       at: '/'.
529   dispatcher register: [:aReq :anEnv | self showControlListFrameOn: aReq. nil]
530       at: '/control_list_frame'.
531   dispatcher register: [:aReq :anEnv | self showListFrameOn: aReq. nil]
532       at: '/list_frame'.
533   dispatcher register: [:aReq :anEnv | self showControlFrameOn: aReq. nil]
534       at: '/control_frame'.
535   dispatcher register: [:aReq :anEnv | self listPackagesOn: aReq. nil]
536       at: '/package_list_frame'.
537   dispatcher register: [:aReq :anEnv | self listClassesOn: aReq. nil]
538       at: '/class_list_frame'.
539   dispatcher register: [:aReq :anEnv | self listMethodsOn: aReq. nil]
540       at: '/method_list_frame'.
541   dispatcher register: [:aReq :anEnv | self editMethodOn: aReq. nil]
542       at: '/edit_frame'.
543   dispatcher register: [:aReq :anEnv | self compileMethodOn: aReq. nil]
544       at: '/compile_method'.
545   dispatcher register: [:aReq :anEnv | self frameSubclass: aReq. nil]
546       at: '/subclass'.
547   dispatcher register: [:aReq :anEnv | aReq response: '<HTML><BODY><B>Class browser stopped.</B></BODY></HTML>'. dispatcher stop.  aSock close. nil]
548       at: '/stop'.
550   dispatcher registerErrorHandler: [ :aReq :anEnv | self showErrorOn: aReq. nil].
551   dispatcher startOn: aSock.
552   ^nil.
555 ^start [
556   | sock obj cnt |
557   "create a default socket on which to listen"
558   cnt := 20.
559   [ cnt > 0 ] whileTrue: [
560     sock := TCPSocket new.
561     (sock bind: '127.0.0.1' port: 6789) < 0 ifFalse: [
562       'starting...' printNl.
563       obj := self new.
564       ^obj startOn: sock.
565     ].
566     sock close.
567     'binding failed.' printNl.
568     cnt := cnt - 1.
569     System sleep: 3.
570   ].
571   self error: 'can''t start browser: binding error'.
576 { HTTPClassBrowser start }