├── .gitignore
├── LICENSE
├── README.md
├── examples
└── MessageBoard
│ ├── Main.idr
│ ├── Makefile
│ └── db.sql
├── idris_web.ipkg
├── paper
├── conclusion.tex
├── effects.tex
├── forms.tex
├── introduction.tex
├── main.tex
├── messageboard.tex
├── protocols.tex
├── refs.bib
└── sigplanconf.cls
└── src
├── IdrisWeb
├── CGI
│ ├── Cgi.idr
│ ├── CgiTypes.idr
│ ├── CgiUtils.idr
│ ├── formhello.idr
│ ├── helloworld.idr
│ └── test.idr
├── Common
│ ├── Date.idr
│ ├── Parser.idr
│ └── Random
│ │ ├── Makefile
│ │ └── RandC.idr
├── DB
│ └── SQLite
│ │ ├── SQLiteCodes.idr
│ │ ├── SQLiteNew.idr
│ │ └── SQLiteTest.idr
├── Form
│ ├── FormTest.idr
│ ├── FormTypes.idr
│ └── frmtest.idr
├── GCrypt
│ ├── GCrypt.idr
│ ├── gcrypt_idr.c
│ └── gcrypt_idr.h
└── Session
│ ├── Session.idr
│ ├── SessionUtils.idr
│ └── sessiondb.sql
├── MakefileC
├── rand_c.c
├── rand_c.h
├── sqlite3api.c
└── sqlite3api.h
/.gitignore:
--------------------------------------------------------------------------------
1 | *.ibc
2 | *.o
3 | *.swp
4 | *.db
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Simon Fowler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | IdrisWeb
2 | ========
3 |
4 | A secure web framework, built in the Idris language.
5 |
--------------------------------------------------------------------------------
/examples/MessageBoard/Main.idr:
--------------------------------------------------------------------------------
1 | module Main
2 | import Effects
3 | import IdrisWeb.CGI.Cgi
4 | import IdrisWeb.Session.Session
5 | import IdrisWeb.Session.SessionUtils
6 | import IdrisWeb.DB.SQLite.SQLiteNew
7 |
8 | ThreadID : Type
9 | ThreadID = Int
10 |
11 | DB_NAME : String
12 | DB_NAME = "/tmp/messageboard.db"
13 |
14 | UserID : Type
15 | UserID = Int
16 |
17 | USERID_VAR : String
18 | USERID_VAR = "user_id"
19 |
20 | ----------
21 | -- Handler info
22 | ----------
23 | handleRegisterForm : Maybe String -> Maybe String -> FormHandler [CGI (InitialisedCGI TaskRunning),
24 | SESSION (SessionRes SessionUninitialised),
25 | SQLITE ()
26 | ]
27 |
28 | handlePost : Maybe Int -> Maybe String -> FormHandler [CGI (InitialisedCGI TaskRunning),
29 | SESSION (SessionRes SessionUninitialised),
30 | SQLITE ()
31 | ]
32 |
33 | handleNewThread : Maybe String -> Maybe String -> FormHandler [CGI (InitialisedCGI TaskRunning),
34 | SESSION (SessionRes SessionUninitialised),
35 | SQLITE ()
36 | ]
37 |
38 | handleLoginForm : Maybe String -> Maybe String -> FormHandler [CGI (InitialisedCGI TaskRunning),
39 | SESSION (SessionRes SessionUninitialised),
40 | SQLITE ()
41 | ]
42 |
43 | handlers : HandlerList
44 | handlers = [(([FormString, FormString], [CgiEffect, SessionEffect, SqliteEffect]) ** (handleRegisterForm, "handleRegisterForm")),
45 | (([FormString, FormString], [CgiEffect, SessionEffect, SqliteEffect]) ** (handleLoginForm, "handleLoginForm")),
46 | (([FormString, FormString], [CgiEffect, SessionEffect, SqliteEffect]) ** (handleNewThread, "handleNewThread")),
47 | (([FormInt, FormString], [CgiEffect, SessionEffect, SqliteEffect]) ** (handlePost, "handlePost"))]
48 |
49 |
50 | -- Template system would be nice...
51 | htmlPreamble : String
52 | htmlPreamble = "
IdrisWeb Message Board"
53 |
54 | htmlPostamble : String
55 | htmlPostamble = ""
56 |
57 | notLoggedIn : EffM IO [CGI (InitialisedCGI TaskRunning),
58 | SESSION (SessionRes SessionInitialised),
59 | SQLITE ()]
60 | [CGI (InitialisedCGI TaskRunning),
61 | SESSION (SessionRes SessionUninitialised),
62 | SQLITE ()] ()
63 | notLoggedIn = do output htmlPreamble
64 | output "Error
You must be logged in to do that!"
65 | output htmlPostamble
66 | discardSession
67 |
68 | outputWithPreamble : String -> Eff IO [CGI (InitialisedCGI TaskRunning)] ()
69 | outputWithPreamble txt = do output htmlPreamble
70 | output txt
71 | output htmlPostamble
72 | -----------
73 | -- Post Creation
74 | -----------
75 | postInsert : Int -> Int -> String -> Eff IO [SQLITE ()] Bool
76 | postInsert uid thread_id content = do
77 | conn_res <- openDB DB_NAME
78 | if_valid then do
79 | let sql = "INSERT INTO `Posts` (`UserID`, `ThreadID`, `Content`) VALUES (?, ?, ?)"
80 | ps_res <- prepareStatement sql
81 | if_valid then do
82 | bindInt 1 uid
83 | bindInt 2 thread_id
84 | bindText 3 content
85 | bind_res <- finishBind
86 | if_valid then do
87 | executeStatement
88 | finalise
89 | closeDB
90 | return True
91 | else do
92 | cleanupBindFail
93 | return False
94 | else do
95 | cleanupPSFail
96 | return False
97 | else
98 | return False
99 |
100 |
101 |
102 | addPostToDB : Int -> String -> SessionData -> EffM IO [CGI (InitialisedCGI TaskRunning),
103 | SESSION (SessionRes SessionInitialised),
104 | SQLITE ()]
105 | [CGI (InitialisedCGI TaskRunning),
106 | SESSION (SessionRes SessionUninitialised),
107 | SQLITE ()] ()
108 | addPostToDB thread_id content sd = do
109 | -- TODO: would be nice to abstract this out
110 | case lookup USERID_VAR sd of
111 | Just (SInt uid) => do insert_res <- postInsert uid thread_id content
112 | if insert_res then do
113 | -- TODO: redirection would be nice
114 | outputWithPreamble "Post successful"
115 | discardSession
116 | return ()
117 | else do
118 | outputWithPreamble "There was an error adding the post to the database."
119 | discardSession
120 | return ()
121 | Nothing => do notLoggedIn
122 | return ()
123 |
124 |
125 |
126 | handlePost (Just thread_id) (Just content) = do withSession (addPostToDB thread_id content) notLoggedIn
127 | pure ()
128 | handlePost _ _ = do outputWithPreamble"Error
There was an error processing your post."
129 | pure ()
130 |
131 | newPostForm : Int -> UserForm
132 | newPostForm thread_id = do
133 | addHidden FormInt thread_id
134 | addTextBox "Post Content" FormString Nothing
135 | useEffects [CgiEffect, SessionEffect, SqliteEffect]
136 | addSubmit handlePost handlers
137 |
138 |
139 | showNewPostForm : Int -> CGIProg [SESSION (SessionRes SessionUninitialised), SQLITE ()] ()
140 | showNewPostForm thread_id = do
141 | output htmlPreamble
142 | output "Create new post
"
143 | addForm (newPostForm thread_id)
144 | output htmlPostamble
145 |
146 | -----------
147 | -- Thread Creation
148 | -----------
149 |
150 | threadInsert : Int -> String -> String -> Eff IO [SQLITE ()] (Maybe QueryError)
151 | threadInsert uid title content = do
152 | let query = "INSERT INTO `Threads` (`UserID`, `Title`) VALUES (?, ?)"
153 | insert_res <- executeInsert DB_NAME query [(1, DBInt uid), (2, DBText title)]
154 | case insert_res of
155 | Left err => return (Just err)
156 | Right thread_id => do
157 | post_res <- postInsert uid thread_id content
158 | if post_res then return Nothing else return $ Just (ExecError "post")
159 |
160 |
161 | addNewThread : String -> String -> SessionData -> EffM IO [CGI (InitialisedCGI TaskRunning),
162 | SESSION (SessionRes SessionInitialised),
163 | SQLITE ()]
164 | [CGI (InitialisedCGI TaskRunning),
165 | SESSION (SessionRes SessionUninitialised),
166 | SQLITE ()] ()
167 | addNewThread title content sd = do
168 | case lookup USERID_VAR sd of
169 | Just (SInt uid) =>
170 | do insert_res <- threadInsert uid title content
171 | case insert_res of
172 | Just err => do
173 | output $ "There was an error adding the thread to the database: " ++ show err
174 | discardSession
175 | return ()
176 | Nothing => do
177 | output "Thread added successfully"
178 | discardSession
179 | return ()
180 | Nothing => do notLoggedIn
181 | return ()
182 |
183 | -- Create a new thread, given the title and content
184 | handleNewThread (Just title) (Just content) = do withSession (addNewThread title content) notLoggedIn
185 | pure ()
186 | handleNewThread _ _ = do outputWithPreamble "Error
There was an error posting your thread."
187 | pure ()
188 |
189 | newThreadForm : UserForm
190 | newThreadForm = do
191 | addTextBox "Title" FormString Nothing
192 | addTextBox "Post Content" FormString Nothing -- password field would be good
193 | useEffects [CgiEffect, SessionEffect, SqliteEffect]
194 | addSubmit handleNewThread handlers
195 |
196 |
197 | showNewThreadForm : CGIProg [SESSION (SessionRes SessionUninitialised), SQLITE ()] ()
198 | showNewThreadForm = do output htmlPreamble
199 | output "New Thread
"
200 | addForm newThreadForm
201 | output htmlPostamble
202 |
203 |
204 | -----------
205 | -- Registration
206 | -----------
207 |
208 | insertUser : String -> String -> Eff IO [SQLITE ()] (Either QueryError Int)
209 | insertUser name pwd = executeInsert DB_NAME query bind_vals
210 | where query = "INSERT INTO `Users` (`Username`, `Password`) VALUES (?, ?)"
211 | bind_vals = [(1, DBText name), (2, DBText pwd)]
212 |
213 |
214 | userExists' : EffM IO [SQLITE (Either (SQLiteExecuting InvalidRow) (SQLiteExecuting ValidRow))]
215 | [SQLITE ()] Bool
216 | userExists' =
217 | if_valid then do
218 | finaliseValid
219 | closeDB
220 | return True
221 | else do
222 | finaliseInvalid
223 | closeDB
224 | return False
225 |
226 |
227 | userExists : String -> Eff IO [SQLITE ()] (Either QueryError Bool)
228 | userExists username = do
229 | conn_res <- openDB DB_NAME
230 | if_valid then do
231 | let sql = "SELECT * FROM `Users` WHERE `Username` = ?"
232 | ps_res <- prepareStatement sql
233 | if_valid then do
234 | bindText 1 username
235 | bind_res <- finishBind
236 | if_valid then do
237 | executeStatement
238 | res <- userExists'
239 | return $ Right res
240 | else do
241 | let be = getBindError bind_res
242 | cleanupBindFail
243 | return $ Left be
244 | else do
245 | cleanupPSFail
246 | return $ Left (getQueryError ps_res)
247 | else
248 | return $ Left (getQueryError conn_res)
249 |
250 |
251 | handleRegisterForm (Just name) (Just pwd) = do
252 | user_exists_res <- userExists name
253 | case user_exists_res of
254 | Left err => do outputWithPreamble "Error checking for user existence"
255 | pure ()
256 | Right user_exists =>
257 | if (not user_exists) then do
258 | insert_res <- insertUser name pwd
259 | case insert_res of
260 | Left err => do outputWithPreamble ("Error inserting new user" ++ (show err))
261 | pure ()
262 | Right insert_res => do outputWithPreamble "User created successfully!"
263 | pure ()
264 | else do outputWithPreamble "This user already exists; please pick another name!"
265 | pure ()
266 |
267 | handleRegisterForm _ _ = do outputWithPreamble "Error processing form input data."
268 | pure ()
269 |
270 | registerForm : UserForm
271 | registerForm = do
272 | addTextBox "Username" FormString Nothing
273 | addTextBox "Password" FormString Nothing -- password field would be good
274 | useEffects [CgiEffect, SessionEffect, SqliteEffect]
275 | addSubmit handleRegisterForm handlers
276 |
277 | showRegisterForm : CGIProg [SESSION (SessionRes SessionUninitialised), SQLITE ()] ()
278 | showRegisterForm = do output htmlPreamble
279 | output "Create a new account
"
280 | addForm registerForm
281 | output htmlPostamble
282 |
283 | -----------
284 | -- Login
285 | -----------
286 | alreadyLoggedIn : SessionData ->
287 | EffM IO [CGI (InitialisedCGI TaskRunning),
288 | SESSION (SessionRes SessionInitialised),
289 | SQLITE ()]
290 | [CGI (InitialisedCGI TaskRunning),
291 | SESSION (SessionRes SessionUninitialised),
292 | SQLITE ()] ()
293 | alreadyLoggedIn _ = do outputWithPreamble "Error
You appear to already be logged in!"
294 | discardSession
295 | -- If the credentials match, return an ID
296 | -- Maybe consolidate the Maybe UserID into the Either, or possibly keep them
297 | -- distinct to encapsulate the system error vs auth failure
298 | authUser' : EffM IO [SQLITE (Either (SQLiteExecuting InvalidRow)
299 | (SQLiteExecuting ValidRow))]
300 | [SQLITE ()]
301 | (Either QueryError (Maybe UserID))
302 | authUser' =
303 | if_valid then do
304 | user_id <- getColumnInt 0
305 | finaliseValid
306 | closeDB
307 | return $ Right (Just user_id)
308 | else do
309 | finaliseInvalid
310 | closeDB
311 | return $ Right Nothing
312 |
313 | authUser : String -> String -> Eff IO [SQLITE ()] (Either QueryError (Maybe UserID))
314 | authUser username password = do
315 | conn_res <- openDB DB_NAME
316 | if_valid then do
317 | let sql = "SELECT `UserID` FROM `Users` WHERE `Username` = ? AND `Password` = ?"
318 | ps_res <- prepareStatement sql
319 | if_valid then do
320 | bindText 1 username
321 | bindText 2 password
322 | bind_res <- finishBind
323 | if_valid then do
324 | executeStatement
325 | authUser'
326 | else do
327 | let be = getBindError bind_res
328 | cleanupBindFail
329 | return $ Left be
330 | else do
331 | cleanupPSFail
332 | return $ Left (getQueryError ps_res)
333 | else
334 | return $ Left (getQueryError conn_res)
335 |
336 |
337 | setSession : UserID -> Eff IO [CGI (InitialisedCGI TaskRunning), SESSION (SessionRes SessionUninitialised), SQLITE ()] Bool
338 | setSession user_id = do
339 | create_res <- createSession [(USERID_VAR, SInt user_id)]
340 | sess_res <- setSessionCookie
341 | db_res <- writeSessionToDB
342 | return (sess_res && db_res)
343 |
344 |
345 | handleLoginForm (Just name) (Just pwd) = do
346 | auth_res <- authUser name pwd
347 | case auth_res of
348 | Right (Just uid) => do
349 | set_sess_res <- setSession uid
350 | if set_sess_res then do
351 | output $ "Welcome, " ++ name
352 | return ()
353 | else do
354 | output "Could not set session"
355 | return ()
356 | Right Nothing => do
357 | output "Invalid username or password"
358 | return ()
359 | Left err => do
360 | output $ "Error: " ++ (show err)
361 | return ()
362 |
363 |
364 | loginForm : UserForm
365 | loginForm = do
366 | addTextBox "Username" FormString Nothing
367 | addTextBox "Password" FormString Nothing -- password field would be good
368 | useEffects [CgiEffect, SessionEffect, SqliteEffect]
369 | addSubmit handleLoginForm handlers
370 |
371 | showLoginForm : CGIProg [SESSION (SessionRes SessionUninitialised), SQLITE ()] ()
372 | showLoginForm = do output htmlPreamble
373 | output "Log in
"
374 | addForm loginForm
375 | output "