Skip to content

Commit 6ae6fe4

Browse files
committed
Refactor WSGI app
1 parent 65e064a commit 6ae6fe4

File tree

1 file changed

+278
-73
lines changed
  • src/grongier/cls/Grongier/Service

1 file changed

+278
-73
lines changed
Lines changed: 278 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Class Grongier.Service.WSGI Extends (%RegisteredObject, %CSP.REST) [ ServerOnly = 1 ]
1+
Class Grongier.Service.WSGI Extends %CSP.REST [ ServerOnly = 1 ]
22
{
33

44
Parameter CLASSPATHS;
@@ -7,99 +7,304 @@ Parameter MODULENAME;
77

88
Parameter APPNAME;
99

10-
/// Helper method to write data to the output stream
11-
ClassMethod write(data)
12-
{
13-
w data
14-
}
10+
Parameter DEVMODE = 1;
1511

16-
/// Helper to build the environ
17-
ClassMethod GetEnviron() As %SYS.Python
12+
/// This method matches the request and method and calls the dispatcher
13+
ClassMethod Page(skipheader As %Boolean = 0) As %Status [ ProcedureBlock = 1 ]
1814
{
19-
//set builtins
20-
set builtins = ##class(%SYS.Python).Import("builtins")
15+
#dim tSC As %Status = $$$OK
16+
#dim e As %Exception.AbstractException
17+
18+
#dim tAuthorized,tRedirected As %Boolean
19+
#dim tRedirectRoutine,tURL As %String = ""
20+
#dim %response As %CSP.Response
21+
22+
Try {
2123

22-
//import dict to create environ
23-
set dict = builtins.dict()
24+
#; Ensure that we honor the requested charset
25+
Set %response.CharSet=..#CHARSET
2426

25-
#dim %request As %CSP.Request
27+
#; Ensure that we honor the requested CONTENTTYPE
28+
If ..#CONTENTTYPE'="" Set %response.ContentType=..#CONTENTTYPE
2629

27-
//set environ
28-
do dict."__setitem__"("SERVER_NAME", $System.INetInfo.LocalHostName())
29-
do dict."__setitem__"("SERVER_PORT", "")
30-
do dict."__setitem__"("SERVER_PROTOCOL", "HTTP/1.1")
31-
do dict."__setitem__"("SERVER_SOFTWARE", "IRIS")
32-
do dict."__setitem__"("SCRIPT_NAME", ..#APPNAME)
33-
do dict."__setitem__"("REQUEST_METHOD", %request.Method)
34-
do dict."__setitem__"("CONTENT_TYPE", %request.ContentType)
35-
do dict."__setitem__"("CHARSET", %request.CharSet)
36-
Set app=$$getapp^%SYS.cspServer(%request.URL,.path,.match,.updatedurl)
37-
do dict."__setitem__"("PATH_INFO", $extract(updatedurl,$length(path),*))
30+
#; Ensure that we honor the requested HTTP_ACCEPT_LANGUAGE
31+
Set %response.Domain = ..#DOMAIN
32+
Do %response.MatchLanguage()
33+
34+
#; Record if device re-direction is already active
35+
Set tRedirected=##class(%Library.Device).ReDirectIO()
36+
37+
#; Record the redirect routine
38+
Set tRedirectRoutine=$System.Device.GetMnemonicRoutine()
39+
40+
if ..#DEVMODE {
41+
#; Not so pretty but help to for reload the WSGI application
42+
do outputError^%SYS.cspServer2("","","","")
43+
}
44+
else {
45+
#; Now switch to using THIS routine for device redirection
46+
Use $io::("^%SYS.cspServer2")
3847

39-
// to extract the query string
48+
#; Switch device redirection on (may already be on but thats ok)
49+
Do ##class(%Library.Device).ReDirectIO(1)
50+
}
4051

41-
return dict
42-
}
52+
#; Ensure that the application is defined (security check)
53+
If $$$GetSecurityApplicationsDispatchClass(%request.AppData)="" {
54+
55+
#; Report not authorized
56+
Set tSC=..Http403()
57+
58+
#; Done
59+
Quit
60+
}
61+
62+
#; GgiEnvs are not defined in the CSP shell
63+
Set tURL=$Get(%request.CgiEnvs("CSPLIB"))
64+
If tURL="" Set tURL=%request.URL
65+
66+
#; Do an access check
67+
Set tSC=..AccessCheck(.tAuthorized)
68+
If $$$ISERR(tSC) Quit
4369

44-
/// Implement a singleton pattern to get the python app
45-
ClassMethod GetPyhonApp() As %SYS.Python
46-
{
47-
if ..#CLASSPATHS '="" {
48-
set sys = ##class(%SYS.Python).Import("sys")
49-
set delimiter = $s($system.Version.GetOS()="Windows":";",1:":")
50-
set extraClasspaths = $tr(..#CLASSPATHS,delimiter,"|")
51-
for i=1:1:$l(extraClasspaths,"|") {
52-
set onePath = $p(extraClasspaths,"|",i)
53-
set onePath = ##class(%File).NormalizeDirectory(onePath)
54-
if onePath?1"$$IRISHOME"1P.E set onePath = $e($system.Util.InstallDirectory(),1,*-1)_$e(onePath,11,*)
55-
if onePath'="" do sys.path.append(onePath)
70+
If tAuthorized=0 {
71+
72+
#; Don't want the session token
73+
Set %response.OutputSessionToken=0
74+
75+
#; Set the Http Status
76+
Set %response.Status=..#HTTP403FORBIDDEN
77+
78+
#; Done
79+
Quit
5680
}
81+
82+
#; Extract the match url from the application name
83+
Set tMatchUrl = "/"_$Extract(tURL,$Length(%request.Application)+1,*)
84+
85+
#; Dispatch the request
86+
Set tSC=..DispatchRequest(tMatchUrl,%request.Method)
87+
88+
} Catch (e) {
89+
Set tSC=e.AsStatus()
90+
}
91+
92+
If $$$ISERR(tSC) {
93+
94+
#; Don't want the session token
95+
Set %response.OutputSessionToken=0
96+
97+
Do ..Http500(##class(%Exception.StatusException).CreateFromStatus(tSC))
98+
}
99+
100+
#; Ensure that at least something is written out as the body
101+
#; This will trigger the device redirect capture and force headers to be written
102+
#; (if not already done)
103+
Write ""
104+
105+
#; Reset redirect device if necessary
106+
If tRedirected {
107+
108+
#; Use the original redirected routine
109+
Use $io::("^"_tRedirectRoutine)
110+
111+
#; Switch device redirection on
112+
Do ##class(%Library.Device).ReDirectIO(1)
57113
}
114+
115+
#; Any errors should have been caught and reported
116+
Quit $$$OK
117+
}
58118

59-
//import module
60-
set module = ##class(%SYS.Python).Import(..#MODULENAME)
119+
ClassMethod OnPreDispatch(
120+
pUrl As %String,
121+
pMethod As %String,
122+
ByRef pContinue As %Boolean) As %Status
123+
{
124+
Set path = ..#CLASSPATHS
125+
Set appName = ..#APPNAME
126+
Set module = ..#MODULENAME
127+
Set devmode = ..#DEVMODE
128+
Set pContinue = 1
129+
Do ..DispatchREST(pUrl, path, appName, module, devmode)
130+
Quit $$$OK
131+
}
61132

62-
//set builtins
63-
set builtins = ##class(%SYS.Python).Import("builtins")
133+
ClassMethod DispatchREST(
134+
PathInfo As %String,
135+
appPath As %String,
136+
appName As %String,
137+
module As %String,
138+
devmode As %Boolean = 1) As %Status
139+
{
140+
Set builtins = ##CLASS(%SYS.Python).Builtins()
141+
Set interface = ##CLASS(%SYS.Python).Import("grongier.pex.wsgi.handlers")
142+
Set rawformdata = ""
143+
Set environ = builtins.dict()
144+
Set key = %request.NextCgiEnv("")
64145

65-
//set app
66-
set application = builtins.getattr(module, ..#APPNAME)
146+
// Let's check if the WSGI application has been loaded or not for this session.
147+
148+
If (($DATA(%session.Data("application")) && $ISOBJECT(%session.Data("application"))) && 'devmode) {
149+
Set application = %session.Data("Application")
150+
}
151+
Else{
67152

68-
Return application
69-
}
153+
Set application = ..GetPythonClass(appName, module, appPath)
154+
If application = "" {
155+
throw ##class(%Exception.General).%New("Error loading WSGI application: "_module_"."_appName_" from "_appPath)
156+
}
157+
Else {
158+
Set %session.Data("Application") = application
159+
}
160+
}
70161

71-
ClassMethod Page(skipheader As %Boolean = 1) As %Status [ Internal, ServerOnly = 1 ]
72-
{
73-
Try {
74-
75-
//set environ
76-
set environ = ..GetEnviron()
162+
// Editing some CGI variables that may be incorrect in %request
163+
// Also filling in environ with as many CGI variables as possible from %request
164+
// WSGI states that all CGI variables are valid and should be included if possible
165+
While (key'="") {
166+
Set value = %request.GetCgiEnv(key)
167+
If key = "PATH_INFO" {
168+
Set app=$$getapp^%SYS.cspServer(%request.URL,.path,.match,.updatedurl)
169+
Set value = $EXTRACT(updatedurl,$LENGTH(path),*)
170+
}
171+
If key = "SCRIPT_NAME" {
172+
//%request will sometimes have Script_name include Path_info
173+
Set value = $PIECE(%request.Application, "/",1,*-1)
174+
}
175+
Do environ."__setitem__"(key,value)
176+
Set key = %request.NextCgiEnv(key)
177+
}
77178

78-
//import sys
79-
set sys = ##class(%SYS.Python).Import("sys")
179+
//Have to set up a correct wsgi.input stream from %request
180+
Set stream = %request.Content
181+
182+
Set contentType = %request.ContentType
183+
Set contentLength = 0
80184

81-
//set stdin
82-
set builtins = ##class(%SYS.Python).Import("builtins")
83-
set ba = builtins.bytearray()
84-
85-
while %request.Content.AtEnd = 0 {
86-
do ba.extend(##class(%SYS.Python).Bytes(%request.Content.Read()))
87-
}
88-
//set handler
89-
set handler = ##class(%SYS.Python).Import("grongier.pex.wsgi.handlers").IrisHandler(ba, sys.stdout, sys.stderr,environ)
185+
If contentType = "application/x-www-form-urlencoded" {
186+
Set formdict = builtins.dict()
187+
Set key = $ORDER(%request.Data(""))
188+
While (key'="") {
189+
Set value = $GET(%request.Data(key,1))
190+
Do formdict."__setitem__"(key,value)
191+
Set key = $ORDER(%request.Data(key))
192+
}
193+
Do environ."__setitem__"("formdata", formdict)
194+
}
195+
ElseIf contentType = "multipart/form-data" {
196+
Set boundary = $PIECE(%request.GetCgiEnv("CONTENT_TYPE"), "=",2)
197+
Set stream = ##CLASS(%CSP.BinaryStream).%New()
90198

91-
// get a singleton app
92-
set application = ..GetPyhonApp()
199+
Do stream.Write($CHAR(13,10))
93200

94-
//run app
95-
do handler.run(application)
201+
//Get the Form Data values
96202

97-
}
98-
Catch ex {
99-
return ex.AsStatus()
100-
}
203+
Set key = $ORDER(%request.Data(""))
204+
While (key'="") {
205+
Do stream.Write("--")
206+
Do stream.Write(boundary)
207+
Do stream.Write($CHAR(13,10))
208+
Set value = $GET(%request.Data(key,1))
209+
Do stream.Write("Content-Disposition: form-data; name=")
210+
Do stream.Write(""""_key_"""")
211+
Do stream.Write($CHAR(13,10,13,10))
212+
Do stream.Write(value)
213+
Do stream.Write($CHAR(13,10))
214+
Set key = $ORDER(%request.Data(key))
215+
}
216+
217+
//Now get the possible MIME data streams
218+
Set key = %request.NextMimeData("")
219+
While key'="" {
220+
Set numMimeStreams = %request.CountMimeData(key)
221+
Set index = %request.NextMimeDataIndex(key, "")
222+
Do stream.Write("--")
223+
Do stream.Write(boundary)
224+
Do stream.Write($CHAR(13,10))
225+
If numMimeStreams > 1 {
226+
//I need to create a boundary for a nested multipart content type
227+
Set internalboundary = "--"
228+
For i = 1 : 1 : 7 {
229+
Set internalboundary = internalboundary _ $RANDOM(10)
230+
}
231+
While index '= "" {
232+
Set mimestream = %request.GetMimeData(key, index)
233+
Set headers = mimestream.Headers
234+
Do stream.Write("--")
235+
Do stream.Write(internalboundary)
236+
Do stream.Write($CHAR(13,10))
237+
Do stream.Write(headers)
238+
Do stream.Write($CHAR(13,10,13,10))
239+
Set sc = stream.CopyFrom(mimestream)
240+
//TODO error handling
241+
Do stream.Write($CHAR(13,10))
242+
Set index = %request.NextMimeDataIndex(key, index)
243+
}
244+
Do stream.Write("--")
245+
Do stream.Write(internalboundary)
246+
Do stream.Write("--")
247+
}
248+
Else {
249+
Set mimestream = %request.GetMimeData(key, index)
250+
Set headers = mimestream.Headers
251+
Do stream.Write(headers)
252+
Do stream.Write($CHAR(13,10,13,10))
253+
Set sc = stream.CopyFrom(mimestream)
254+
//TODO error handling
255+
Do stream.Write($CHAR(13,10))
256+
}
257+
Set key = %request.NextMimeData(key)
258+
}
259+
Do stream.Write("--")
260+
Do stream.Write(boundary)
261+
Do stream.Write("--")
262+
Do stream.Rewind()
263+
}
264+
265+
266+
Try {
267+
Do interface."make_request"(environ, stream, application, appPath)
268+
}
269+
Catch exception {
270+
throw exception
271+
}
272+
Quit $$$OK
273+
}
274+
275+
ClassMethod GetPythonClass(
276+
pClassname As %String,
277+
pModule As %String,
278+
pClasspath As %String) As %SYS.Python
279+
{
280+
Try {
281+
If pClasspath '="" {
282+
set sys = ##class(%SYS.Python).Import("sys")
283+
284+
for i=0:1:(sys.path."__len__"()-1) {
285+
Try {
286+
if sys.path."__getitem__"(i) = pClasspath {
287+
do sys.path."__delitem__"(i)
288+
}
289+
}
290+
Catch ex {
291+
// do nothing
292+
}
293+
294+
}
295+
do sys.path.insert(0, pClasspath)
296+
}
101297

102-
quit $$$OK
298+
Set importlib = ##class(%SYS.Python).Import("importlib")
299+
Set builtins = ##class(%SYS.Python).Import("builtins")
300+
Set module = importlib."import_module"(pModule)
301+
Set class = builtins.getattr(module, pClassname)
302+
}
303+
Catch ex {
304+
throw ##class(%Exception.General).%New("Error loading WSGI application: "_pModule_"."_pClassname_" from "_pClasspath)
305+
}
306+
307+
Quit class
103308
}
104309

105310
}

0 commit comments

Comments
 (0)