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 (deprecated) 148 /+ 149 { 150 string hex; 151 do 152 hex = 153 uniform(3, 5) 154 .iota 155 .map!(i => "xX".sample) 156 .map!(f => 157 [1, 2, 4].sample 158 .iota 159 .map!(j => 160 format("%02" ~ f, uniform(0, 0x100)) 161 ) 162 .join("") 163 ) 164 .join(" "); 165 while (hex.length > 20); 166 code = 167 q{ 168 string A = x"CC"; 169 }.formatExample() 170 .replace("A", identifiers.pluck) 171 .replace("CC", hex) 172 ; 173 answers = cartesianJoin(["hex", "hex ", "hexadecimal "], ["string", "strings"], ["", " literal", " literals"]); 174 return true; 175 },+/ 176 // associative arrays 177 { 178 string[] types = ["int", "string"]; 179 code = 180 q{ 181 T[U] A; 182 }.formatExample() 183 .replace("A", identifiers.pluck) 184 .replace("T", types.sample) 185 .replace("U", types.sample) 186 ; 187 answers = ["AA", "associative array", "hashmap"]; 188 return true; 189 }, 190 // array slicing 191 { 192 code = 193 q{ 194 A = B[X..Y]; 195 }.formatExample() 196 .replace("A", identifiers.pluck) 197 .replace("B", identifiers.pluck) 198 .replace("X", uniform(0, 5).text) 199 .replace("Y", uniform(5, 10).text) 200 ; 201 answers = cartesianJoin(["", "array "], ["slice", "slicing"]); 202 return true; 203 }, 204 ].randomCover().map!(f => f()).any(); 205 }, 206 // Calculate function result 207 // (use syntax that only programmers should be familiar with) 208 { 209 question = "What will be the return value of the following function?"; 210 hint = `You can run D code online on <a href="https://run.dlang.io/">run.dlang.io</a>.`; 211 return 212 [ 213 // Modulo operator (%) 214 { 215 int x, y; 216 do 217 { 218 x = specUniform(10, 50); 219 y = specUniform(x/4, x/2); 220 } 221 while (x % y == 0); 222 223 code = 224 q{ 225 int F() 226 { 227 int A = X; 228 A %= Y; 229 return A; 230 } 231 }.formatExample() 232 .replace("F", identifiers.pluck) 233 .replace("A", identifiers.pluck) 234 .replace("X", x.text) 235 .replace("Y", y.text) 236 ; 237 answers = [(x % y).text]; 238 return true; 239 }, 240 // Integer division, increment 241 { 242 int y = specUniform(2, 5); 243 int x = uniform(5, 50*upperFactor / y) * y + specUniform(1, y); 244 int sign = uniform(0, 2) ? -1 : 1; 245 code = 246 q{ 247 int F() 248 { 249 int A = X, B = Y; 250 B@@; 251 A /= B; 252 return A; 253 } 254 }.formatExample() 255 .replace("F", identifiers.pluck) 256 .replace("A", identifiers.pluck) 257 .replace("B", identifiers.pluck) 258 .replace("X", x.text) 259 .replace("Y", (y - sign).text) 260 .replace("@", sign > 0 ? "+" : "-") 261 ; 262 answers = [(x / y).text]; 263 return true; 264 }, 265 // Ternary operator + division/modulo 266 { 267 int x = specUniform(10, 50); 268 int y = specUniform(2, 4); 269 int a = specUniform(10, 50); 270 int b = uniform(2*lowerFactor, a/3); 271 int d = specUniform(5, 10); 272 int c = uniform(2, 50*upperFactor / d) * d + uniform(1, d); 273 code = 274 q{ 275 int F() 276 { 277 return X % Y 278 ? A / B 279 : C % D; 280 } 281 }.formatExample() 282 .replace("F", identifiers.pluck) 283 .replace("X", x.text) 284 .replace("Y", y.text) 285 .replace("A", a.text) 286 .replace("B", b.text) 287 .replace("C", c.text) 288 .replace("D", d.text) 289 ; 290 answers = [(x % y ? a / b : c % d).text]; 291 return true; 292 }, 293 // Formatting, hexadecimal numbers 294 { 295 int n = specUniform(20, 100); 296 n &= ~7; 297 int w = uniform(2, 8); 298 string id = identifiers.pluck; 299 code = 300 q{ 301 string F() 302 { 303 return format("A=%0WX", N); 304 } 305 }.formatExample() 306 .replace("F", identifiers.pluck) 307 .replace("A", id) 308 .replace("N", n.text) 309 .replace("W", w.text) 310 ; 311 answers = [format("%s=%0*X", id, w, n)]; 312 answers ~= answers.map!(s => `"`~s~`"`).array(); 313 return true; 314 }, 315 // iota+reduce - max 316 { 317 int x = specUniform(10, 100); 318 code = 319 q{ 320 int F() 321 { 322 return iota(X).reduce!max; 323 } 324 }.formatExample() 325 .replace("F", identifiers.pluck) 326 .replace("X", x.text) 327 ; 328 answers = [(x - 1).text]; 329 return true; 330 }, 331 // iota+reduce - sum 332 { 333 if (!spec.allowHard) return false; 334 int x = specUniform(3, 10); 335 code = 336 q{ 337 int F() 338 { 339 return iota(X).reduce!"a+b"; 340 } 341 }.formatExample() 342 .replace("F", identifiers.pluck) 343 .replace("X", x.text) 344 ; 345 answers = [(iota(x).reduce!"a+b").text]; 346 return true; 347 }, 348 ].randomCover().map!(f => f()).any(); 349 }, 350 ].randomCover().map!(f => f()).any().enforce("Can't find suitable CAPTCHA"); 351 return challenge; 352 } 353 354 private string[] cartesianJoin(PARTS...)(PARTS parts) 355 { 356 return cartesianProduct(parts).map!(t => join([t.expand])).array(); 357 } 358 359 private string formatExample(string s) 360 { 361 return s 362 .outdent() 363 .strip() 364 .replace("\t", " ") 365 ; 366 }