1 /// A CAPTCHA generator for D services.
2 /// Written in the D programming language.
3 
4 module dcaptcha.dcaptcha;
5 
6 import std.algorithm;
7 import std.conv;
8 import std.exception;
9 import std.random;
10 import std.range;
11 import std..string;
12 
13 import ae.utils.array;
14 
15 import dcaptcha.markov;
16 
17 struct CaptchaSpec
18 {
19 	bool allowEasy = true;
20 	bool allowHard = false;
21 	bool allowStatic = false;
22 }
23 
24 struct Challenge
25 {
26 	string question, code;
27 	string[] answers;
28 	string hint; /// HTML!
29 }
30 
31 /**
32 	Goals:
33 	- Answers should not be obvious:
34 	  - Keywords shouldn't give away the answer, e.g. `unittest { ... }`
35 	    is obviously an unit test block.
36 	  - `return 2+2` is obvious to non-programmers.
37 	- Answers should vary considerably:
38 	  - A question which has the answer "0" much of the time is easy
39 	    to defeat simply by giving the answer "0" all of the time.
40 	- Questions should not be Google-able:
41 	  - Search engines ignore most punctuation and math operators.
42 	  - Keywords and string literals should vary or be generic enough.
43 **/
44 
45 Challenge getCaptcha(CaptchaSpec spec = CaptchaSpec.init)
46 {
47 	string[] identifiers =
48 		26
49 		.iota
50 		.map!(l => [cast(char)('a'+l)].assumeUnique())
51 		.filter!(s => s != "l")
52 		.array();
53 	identifiers ~= "foo, bar, baz".split(", ");
54 	//identifiers ~= "qux, quux, corge, grault, garply, waldo, fred, plugh, xyzzy, thud".split(", ");
55 
56 	enum hardFactor = 100;
57 	int lowerFactor = spec.allowEasy ? 1 : hardFactor;
58 	int upperFactor = spec.allowHard ? hardFactor : 1;
59 
60 	assert(spec.allowEasy || spec.allowHard, "No difficulty selected");
61 
62 	int specUniform(int lowerBound, int upperBound) { return uniform(lowerBound*lowerFactor, upperBound*upperFactor); }
63 
64 	string[] mathOperators = "+ - / * %".split();
65 
66 	Challenge challenge;
67 	with (challenge)
68 		[
69 			// Identify syntax
70 			{
71 				if (!spec.allowStatic || !spec.allowHard)
72 					return false;
73 				question = "What is the name of the D language syntax feature illustrated in the following fragment of D code?";
74 				hint = `You can find the answer in the<br><a href="http://dlang.org/spec.html">Language Reference section of dlang.org</a>.`;
75 				//	`You can find the answers in the <a href="http://dlang.org/lex.html">Lexical</a> and `
76 				//	`<a href="http://dlang.org/grammar.html">Grammar</a> language reference sections on dlang.org.`;
77 				return
78 				[
79 					// lambda
80 					{
81 						code =
82 							q{
83 								(A, B) => A @ B
84 							}.formatExample()
85 							.replace("A", identifiers.pluck)
86 							.replace("B", identifiers.pluck)
87 							.replace("@", mathOperators.pluck)
88 						;
89 						answers = cartesianJoin(["lambda", "lambda function", "anonymous function"], ["", " literal"]);
90 						return true;
91 					},
92 					// static destructor
93 					{
94 						string bye = ["Bye", "Goodbye", "Shutting down", "Exiting"].sample ~ ["", ".", "...", "!"].sample;
95 						code =
96 							q{
97 								static ~this()
98 								{
99 									writeln("BYE");
100 								}
101 							}.formatExample()
102 							.replace("BYE", bye)
103 						;
104 						answers = ["static destructor", "module destructor", "thread destructor"];
105 						return true;
106 					},
107 					// nested comments
108 					{
109 						code =
110 							q{
111 								/+ A = B @ C; /+ A = X; +/ +/
112 							}.formatExample()
113 							.replace("A", identifiers.pluck)
114 							.replace("B", identifiers.pluck)
115 							.replace("C", identifiers.pluck)
116 							.replace("X", uniform(10, 100).text)
117 							.replace("@", mathOperators.pluck)
118 						;
119 						answers = cartesianJoin(["nested ", "nesting "], ["", "block "], ["comment", "comments"]);
120 						return true;
121 					},
122 					// anonymous nested classes
123 					{
124 						code =
125 							q{
126 								auto A = new class O {};
127 							}.formatExample()
128 							.replace("A", identifiers.pluck)
129 							.replace("O", identifiers.pluck.toUpper)
130 						;
131 						answers = cartesianJoin(["anonymous "], ["", "nested "], ["class", "classes"]);
132 						return true;
133 					},
134 					// delimited (heredoc) strings
135 					{
136 						auto delimiter = ["EOF", "DELIM", "STR", "QUOT", "MARK"].sample;
137 						code =
138 							q{
139 								string A = TEXT;
140 							}.formatExample()
141 							.replace("F", identifiers.pluck)
142 							.replace("TEXT", `q"` ~ delimiter ~ "\n" ~ MarkovChain!2.query().join(" ").wrap(38).strip() ~ "\n" ~ delimiter ~ `"`)
143 						;
144 						answers = cartesianJoin(["", "multiline ", "multi-line "], ["delimited", "heredoc"], ["", " string", " strings"]);
145 						return true;
146 					},
147 					// hex strings
148 					{
149 						string hex;
150 						do
151 							hex =
152 								uniform(3, 5)
153 								.iota
154 								.map!(i => "xX".sample)
155 								.map!(f =>
156 									[1, 2, 4].sample
157 									.iota
158 									.map!(j =>
159 										format("%02" ~ f, uniform(0, 0x100))
160 									)
161 									.join("")
162 								)
163 								.join(" ");
164 						while (hex.length > 20);
165 						code =
166 							q{
167 								string A = x"CC";
168 							}.formatExample()
169 							.replace("A", identifiers.pluck)
170 							.replace("CC", hex)
171 						;
172 						answers = cartesianJoin(["hex", "hex ", "hexadecimal "], ["string", "strings"], ["", " literal", " literals"]);
173 						return true;
174 					},
175 					// associative arrays
176 					{
177 						string[] types = ["int", "string"];
178 						code =
179 							q{
180 								T[U] A;
181 							}.formatExample()
182 							.replace("A", identifiers.pluck)
183 							.replace("T", types.sample)
184 							.replace("U", types.sample)
185 						;
186 						answers = ["AA", "associative array", "hashmap"];
187 						return true;
188 					},
189 					// array slicing
190 					{
191 						code =
192 							q{
193 								A = B[X..Y];
194 							}.formatExample()
195 							.replace("A", identifiers.pluck)
196 							.replace("B", identifiers.pluck)
197 							.replace("X", uniform(0, 5).text)
198 							.replace("Y", uniform(5, 10).text)
199 						;
200 						answers = cartesianJoin(["", "array "], ["slice", "slicing"]);
201 						return true;
202 					},
203 				].randomCover().map!(f => f()).any();
204 			},
205 			// Calculate function result
206 			// (use syntax that only programmers should be familiar with)
207 			{
208 				question = "What will be the return value of the following function?";
209 				hint = `You can run D code online on <a href="http://dpaste.dzfl.pl/">DPaste</a>.`;
210 				return
211 				[
212 					// Modulo operator (%)
213 					{
214 						int x, y;
215 						do
216 						{
217 							x = specUniform(10, 50);
218 							y = specUniform(x/4, x/2);
219 						}
220 						while (x % y == 0);
221 
222 						code =
223 							q{
224 								int F()
225 								{
226 									int A = X;
227 									A %= Y;
228 									return A;
229 								}
230 							}.formatExample()
231 							.replace("F", identifiers.pluck)
232 							.replace("A", identifiers.pluck)
233 							.replace("X", x.text)
234 							.replace("Y", y.text)
235 						;
236 						answers = [(x % y).text];
237 						return true;
238 					},
239 					// Integer division, increment
240 					{
241 						int y = specUniform(2, 5);
242 						int x = uniform(5, 50*upperFactor / y) * y + specUniform(1, y);
243 						int sign = uniform(0, 2) ? -1 : 1;
244 						code =
245 							q{
246 								int F()
247 								{
248 									int A = X, B = Y;
249 									B@@;
250 									A /= B;
251 									return A;
252 								}
253 							}.formatExample()
254 							.replace("F", identifiers.pluck)
255 							.replace("A", identifiers.pluck)
256 							.replace("B", identifiers.pluck)
257 							.replace("X", x.text)
258 							.replace("Y", (y - sign).text)
259 							.replace("@", sign > 0 ? "+" : "-")
260 						;
261 						answers = [(x / y).text];
262 						return true;
263 					},
264 					// Ternary operator + division/modulo
265 					{
266 						int x = specUniform(10, 50);
267 						int y = specUniform(2, 4);
268 						int a = specUniform(10, 50);
269 						int b = uniform(2*lowerFactor, a/3);
270 						int d = specUniform(5, 10);
271 						int c = uniform(2, 50*upperFactor / d) * d + uniform(1, d);
272 						code =
273 							q{
274 								int F()
275 								{
276 									return X % Y
277 										? A / B
278 										: C % D;
279 								}
280 							}.formatExample()
281 							.replace("F", identifiers.pluck)
282 							.replace("X", x.text)
283 							.replace("Y", y.text)
284 							.replace("A", a.text)
285 							.replace("B", b.text)
286 							.replace("C", c.text)
287 							.replace("D", d.text)
288 						;
289 						answers = [(x % y ? a / b : c % d).text];
290 						return true;
291 					},
292 					// Formatting, hexadecimal numbers
293 					{
294 						int n = specUniform(20, 100);
295 						n &= ~7;
296 						int w = uniform(2, 8);
297 						string id = identifiers.pluck;
298 						code =
299 							q{
300 								string F()
301 								{
302 									return format("A=%0WX", N);
303 								}
304 							}.formatExample()
305 							.replace("F", identifiers.pluck)
306 							.replace("A", id)
307 							.replace("N", n.text)
308 							.replace("W", w.text)
309 						;
310 						answers = [format("%s=%0*X", id, w, n)];
311 						answers ~= answers.map!(s => `"`~s~`"`).array();
312 						return true;
313 					},
314 					// iota+reduce - max
315 					{
316 						int x = specUniform(10, 100);
317 						code =
318 							q{
319 								int F()
320 								{
321 									return iota(X).reduce!max;
322 								}
323 							}.formatExample()
324 							.replace("F", identifiers.pluck)
325 							.replace("X", x.text)
326 						;
327 						answers = [(x - 1).text];
328 						return true;
329 					},
330 					// iota+reduce - sum
331 					{
332 						if (!spec.allowHard) return false;
333 						int x = specUniform(3, 10);
334 						code =
335 							q{
336 								int F()
337 								{
338 									return iota(X).reduce!"a+b";
339 								}
340 							}.formatExample()
341 							.replace("F", identifiers.pluck)
342 							.replace("X", x.text)
343 						;
344 						answers = [(iota(x).reduce!"a+b").text];
345 						return true;
346 					},
347 				].randomCover().map!(f => f()).any();
348 			},
349 		].randomCover().map!(f => f()).any().enforce("Can't find suitable CAPTCHA");
350 	return challenge;
351 }
352 
353 private string[] cartesianJoin(PARTS...)(PARTS parts)
354 {
355 	return cartesianProduct(parts).map!(t => join([t.expand])).array();
356 }
357 
358 private string formatExample(string s)
359 {
360 	return s
361 		.outdent()
362 		.strip()
363 		.replace("\t", "  ")
364 	;
365 }