├── LICENSE ├── README.md ├── main.go ├── server ├── actor.go ├── clock.go ├── db.go ├── mapping.go ├── recorder.go ├── testrunner.go ├── tests.go ├── testserver.go ├── transport.go ├── webfinger.go └── webserver.go ├── static └── site.css └── templates ├── about.html ├── common.tmpl ├── home.html ├── new_test.html ├── site.tmpl └── test_status.html /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testsuite 2 | 3 | An unofficial partially-automated test suite meant to approximate the official 4 | test suite at [test.activitypub.rocks](http://test.activitypub.rocks/). 5 | 6 | The [official test suite](https://github.com/w3c/activitypub/issues/337) 7 | is known to be down. 8 | 9 | While it would be nice to get that one back up, it also only partially automated 10 | only the C2S tests. This test suite aims to partially automate C2S, S2S, and 11 | common tests. 12 | 13 | Contributions needed & welcome. 14 | 15 | See [this go-fed issue](https://github.com/go-fed/activity/issues/46) 16 | for the old test suite's lists of tests. 17 | 18 | ## How To Use 19 | 20 | Go to [test.activitypub.dev](https://test.activitypub.dev/) and follow the 21 | instructions. 22 | 23 | If you'd like to run it yourself, you will need a free domain name under which 24 | to run this server because sending anything over the localhost interface is 25 | explicitly a failing test case. You will also need a set of TLS keys and 26 | certificates, I recommend [Let's Encrypt](https://letsencrypt.org/). 27 | 28 | ``` 29 | go get github.com/go-fed/testsuite 30 | go install github.com/go-fed/testsuite 31 | ./$GOPATH/bin/testsuite \ 32 | -cert $CERT_FULLCHAIN_FILE \ 33 | -key $TLS_PRIVATE_KEY \ 34 | -host $MY_DNS_HOSTNAME \ 35 | -notify_name $MY_ALIAS \ 36 | -notify_link $LINK_TO_MY_CONTACT_INFO 37 | ``` 38 | 39 | There are other flags but their defaults will suffice. 40 | 41 | ## Status 42 | 43 | In "alpha" development. 44 | 45 | Ready: 46 | 47 | * Common Tests have been ported. Some became split into S2S/C2S test variants. 48 | * Added option for Webfinger to be supported in a test run. 49 | * Some S2S tests. 50 | 51 | Left to do: 52 | 53 | * Continue implementing S2S tests 54 | * Implement all C2S tests 55 | * Add option for verifying inbound HTTP Signatures 56 | * Add option for using outbound HTTP Signatures 57 | 58 | ## Design 59 | 60 | When a new test is started, a temporary TestRunner is set up. It is isolated 61 | from all other TestRunners, with its own in-memory database, and is short-lived 62 | for about fifteen minutes. It also stands up temporary fake Actors, so a test 63 | run is itself a fully-fledged federating S2S ActivityPub application. 64 | 65 | The tests are repeatedly iterated through to self-apply automatically, or 66 | to await further input from the end-user to run more automated tests, or to 67 | await triggers from the end-user's federated software. 68 | 69 | ## Future Improvements 70 | 71 | This testsuite could also host tests for ActivityPub clients in the future, the 72 | "C" side of C2S since go-fed supports the "S" side of both C2S and S2S. These 73 | tests were not included in the original test suite. 74 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "html/template" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "path/filepath" 15 | "time" 16 | 17 | "github.com/go-fed/testsuite/server" 18 | ) 19 | 20 | const ( 21 | kCommonTemplate = "common.tmpl" 22 | kSiteTemplate = "site.tmpl" 23 | kHomePage = "home.html" 24 | kAboutPage = "about.html" 25 | kNewTestPage = "new_test.html" 26 | kTestStatusPage = "test_status.html" 27 | ) 28 | 29 | type CommandLineFlags struct { 30 | CertFile *string 31 | KeyFile *string 32 | Hostname *string 33 | TemplatesDir *string 34 | StaticDir *string 35 | TestTimeout *time.Duration 36 | MaxTests *int 37 | NotifyName *string 38 | NotifyLink *string 39 | LogFile *string 40 | } 41 | 42 | func NewCommandLineFlags() *CommandLineFlags { 43 | c := &CommandLineFlags{ 44 | CertFile: flag.String("cert", "tls.crt", "Path to certificate public key file"), 45 | KeyFile: flag.String("key", "tls.key", "Path to certificate private key file"), 46 | Hostname: flag.String("host", "", "Host name of this instance (including TLD)"), 47 | TemplatesDir: flag.String("templates", "./templates", "Directory containing the Go template files"), 48 | StaticDir: flag.String("static", "./static", "Directory containing statically-served files"), 49 | TestTimeout: flag.Duration("test_timeout", time.Minute*15, "Maximum time tests will be kept"), 50 | MaxTests: flag.Int("max_tests", 30, "Maximum number of concurrent tests"), 51 | NotifyName: flag.String("notify_name", "", "Name of who to notify"), 52 | NotifyLink: flag.String("notify_link", "", "Contact link to who to notify"), 53 | LogFile: flag.String("logfile", "log.txt", "Log file to be able to audit spam & abuse"), 54 | } 55 | flag.Parse() 56 | if err := c.validate(); err != nil { 57 | panic(err) 58 | } 59 | return c 60 | } 61 | 62 | func (c *CommandLineFlags) validate() error { 63 | if len(*c.CertFile) == 0 { 64 | return fmt.Errorf("cert file invalid: %s", *c.CertFile) 65 | } else if len(*c.KeyFile) == 0 { 66 | return fmt.Errorf("key file invalid: %s", *c.KeyFile) 67 | } else if len(*c.NotifyName) == 0 { 68 | return fmt.Errorf("notify_name must be provided") 69 | } else if len(*c.NotifyLink) == 0 { 70 | return fmt.Errorf("notify_link must be provided") 71 | } 72 | return nil 73 | } 74 | 75 | func (c *CommandLineFlags) templateFilepaths(pageFile string) []string { 76 | return []string{ 77 | filepath.Join(*c.TemplatesDir, kCommonTemplate), 78 | filepath.Join(*c.TemplatesDir, kSiteTemplate), 79 | filepath.Join(*c.TemplatesDir, pageFile), 80 | } 81 | } 82 | 83 | func (c *CommandLineFlags) homeTemplate() (*template.Template, error) { 84 | return template.ParseFiles(c.templateFilepaths(kHomePage)...) 85 | } 86 | 87 | func (c *CommandLineFlags) aboutTemplate() (*template.Template, error) { 88 | return template.ParseFiles(c.templateFilepaths(kAboutPage)...) 89 | } 90 | 91 | func (c *CommandLineFlags) newTestTemplate() (*template.Template, error) { 92 | return template.ParseFiles(c.templateFilepaths(kNewTestPage)...) 93 | } 94 | 95 | func (c *CommandLineFlags) testStatusTemplate() (*template.Template, error) { 96 | return template.ParseFiles(c.templateFilepaths(kTestStatusPage)...) 97 | } 98 | 99 | func main() { 100 | c := NewCommandLineFlags() 101 | rand.Seed(time.Now().Unix()) 102 | 103 | tlsConfig := &tls.Config{ 104 | MinVersion: tls.VersionTLS12, 105 | CurvePreferences: []tls.CurveID{tls.CurveP256, tls.X25519}, 106 | PreferServerCipherSuites: true, 107 | CipherSuites: []uint16{ 108 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 109 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 110 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 111 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 112 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 113 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 114 | }, 115 | } 116 | httpsServer := &http.Server{ 117 | Addr: ":https", 118 | TLSConfig: tlsConfig, 119 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), 120 | } 121 | 122 | homeTmpl, err := c.homeTemplate() 123 | if err != nil { 124 | panic(err) 125 | } 126 | aboutTmpl, err := c.aboutTemplate() 127 | if err != nil { 128 | panic(err) 129 | } 130 | newTestTmpl, err := c.newTestTemplate() 131 | if err != nil { 132 | panic(err) 133 | } 134 | testStatusTmpl, err := c.testStatusTemplate() 135 | if err != nil { 136 | panic(err) 137 | } 138 | _ = server.NewWebServer(homeTmpl, aboutTmpl, newTestTmpl, testStatusTmpl, httpsServer, *c.Hostname, *c.TestTimeout, *c.MaxTests, *c.NotifyName, *c.NotifyLink, *c.StaticDir, *c.LogFile) 139 | 140 | redir := &http.Server{ 141 | Addr: ":http", 142 | ReadTimeout: 5 * time.Second, 143 | WriteTimeout: 5 * time.Second, 144 | Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 145 | w.Header().Set("Connection", "close") 146 | http.Redirect(w, req, fmt.Sprintf("https://%s%s", req.Host, req.URL), http.StatusMovedPermanently) 147 | }), 148 | } 149 | go func() { 150 | sigint := make(chan os.Signal, 1) 151 | signal.Notify(sigint, os.Interrupt) 152 | <-sigint 153 | if err := redir.Shutdown(context.Background()); err != nil { 154 | log.Printf("HTTP redirect server Shutdown: %v", err) 155 | } 156 | if err := httpsServer.Shutdown(context.Background()); err != nil { 157 | log.Printf("HTTP server Shutdown: %v", err) 158 | } 159 | }() 160 | go func() { 161 | if err := redir.ListenAndServe(); err != http.ErrServerClosed { 162 | log.Printf("HTTP redirect server ListenAndServe: %v", err) 163 | } 164 | }() 165 | if err := httpsServer.ListenAndServeTLS(*c.CertFile, *c.KeyFile); err != nil { 166 | panic(err) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /server/actor.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/go-fed/activity/pub" 10 | "github.com/go-fed/activity/streams/vocab" 11 | ) 12 | 13 | var _ pub.CommonBehavior = &Actor{} 14 | var _ pub.SocialProtocol = &Actor{} 15 | var _ pub.FederatingProtocol = &Actor{} 16 | 17 | type Actor struct { 18 | db *Database 19 | am *ActorMapping 20 | tr *TestRunner 21 | h pub.HandlerFunc 22 | } 23 | 24 | func NewActor(db *Database, am *ActorMapping, tr *TestRunner, h pub.HandlerFunc) *Actor { 25 | return &Actor{ 26 | db: db, 27 | am: am, 28 | tr: tr, 29 | h: h, 30 | } 31 | } 32 | 33 | /* COMMON BEHAVIORS */ 34 | 35 | func (a *Actor) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 36 | authenticated = true 37 | a.tr.LogAuthenticateGetInbox(c, w, r, authenticated, err) 38 | return 39 | } 40 | 41 | func (a *Actor) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 42 | authenticated = true 43 | a.tr.LogAuthenticateGetOutbox(c, w, r, authenticated, err) 44 | return 45 | } 46 | 47 | func (a *Actor) GetOutbox(c context.Context, r *http.Request) (p vocab.ActivityStreamsOrderedCollectionPage, err error) { 48 | id := HTTPRequestToIRI(r) 49 | p, err = a.db.GetOutbox(c, id) 50 | a.tr.LogGetOutbox(c, r, id, p, err) 51 | return 52 | } 53 | 54 | func (a *Actor) NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t pub.Transport, err error) { 55 | t, err = HTTPSigTransport(c, a.am) 56 | a.tr.LogNewTransport(c, actorBoxIRI, err) 57 | return 58 | } 59 | 60 | func (a *Actor) DefaultCallback(c context.Context, activity pub.Activity) error { 61 | a.tr.LogDefaultCallback(c, activity) 62 | return nil 63 | } 64 | 65 | /* SOCIAL PROTOCOL */ 66 | 67 | func (a *Actor) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) { 68 | a.tr.LogPostOutboxRequestBodyHook(c, r, data) 69 | return c, nil 70 | } 71 | 72 | func (a *Actor) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 73 | authenticated = true 74 | a.tr.LogAuthenticatePostOutbox(c, w, r, authenticated, err) 75 | return 76 | } 77 | 78 | func (a *Actor) SocialCallbacks(c context.Context) (wrapped pub.SocialWrappedCallbacks, other []interface{}, err error) { 79 | wrapped = pub.SocialWrappedCallbacks{ 80 | Create: func(c context.Context, v vocab.ActivityStreamsCreate) error { 81 | a.tr.LogSocialCreate(c, v) 82 | return nil 83 | }, 84 | Update: func(c context.Context, v vocab.ActivityStreamsUpdate) error { 85 | a.tr.LogSocialUpdate(c, v) 86 | return nil 87 | }, 88 | Delete: func(c context.Context, v vocab.ActivityStreamsDelete) error { 89 | a.tr.LogSocialDelete(c, v) 90 | return nil 91 | }, 92 | Follow: func(c context.Context, v vocab.ActivityStreamsFollow) error { 93 | a.tr.LogSocialFollow(c, v) 94 | return nil 95 | }, 96 | Add: func(c context.Context, v vocab.ActivityStreamsAdd) error { 97 | a.tr.LogSocialAdd(c, v) 98 | return nil 99 | }, 100 | Remove: func(c context.Context, v vocab.ActivityStreamsRemove) error { 101 | a.tr.LogSocialRemove(c, v) 102 | return nil 103 | }, 104 | Like: func(c context.Context, v vocab.ActivityStreamsLike) error { 105 | a.tr.LogSocialLike(c, v) 106 | return nil 107 | }, 108 | Undo: func(c context.Context, v vocab.ActivityStreamsUndo) error { 109 | a.tr.LogSocialUndo(c, v) 110 | return nil 111 | }, 112 | Block: func(c context.Context, v vocab.ActivityStreamsBlock) error { 113 | a.tr.LogSocialBlock(c, v) 114 | return nil 115 | }, 116 | } 117 | return 118 | } 119 | 120 | /* FEDERATING PROTOCOL */ 121 | 122 | func (a *Actor) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) (out context.Context, err error) { 123 | out = c 124 | a.tr.LogPostInboxRequestBodyHook(c, r, activity) 125 | return 126 | } 127 | 128 | func (a *Actor) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { 129 | out = a.am.AddContextInfo(c, r) 130 | client := &http.Client{ 131 | Timeout: time.Second * 30, 132 | } 133 | var remoteActor *url.URL 134 | remoteActor, authenticated, err = verifyHttpSignatures(out, a.db.hostname, client, r, a.am) 135 | a.tr.LogAuthenticatePostInbox(out, w, r, remoteActor, authenticated, err) 136 | return 137 | } 138 | 139 | func (a *Actor) Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error) { 140 | blocked = false 141 | a.tr.LogBlocked(c, actorIRIs, blocked, err) 142 | return 143 | } 144 | 145 | func (a *Actor) FederatingCallbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { 146 | wrapped = pub.FederatingWrappedCallbacks{ 147 | OnFollow: pub.OnFollowDoNothing, 148 | Create: func(c context.Context, v vocab.ActivityStreamsCreate) error { 149 | a.tr.LogFederatingCreate(c, v) 150 | return nil 151 | }, 152 | Update: func(c context.Context, v vocab.ActivityStreamsUpdate) error { 153 | a.tr.LogFederatingUpdate(c, v) 154 | return nil 155 | }, 156 | Delete: func(c context.Context, v vocab.ActivityStreamsDelete) error { 157 | a.tr.LogFederatingDelete(c, v) 158 | return nil 159 | }, 160 | Follow: func(c context.Context, v vocab.ActivityStreamsFollow) error { 161 | a.tr.LogFederatingFollow(c, v) 162 | return nil 163 | }, 164 | Accept: func(c context.Context, v vocab.ActivityStreamsAccept) error { 165 | a.tr.LogFederatingAccept(c, v) 166 | return nil 167 | }, 168 | Reject: func(c context.Context, v vocab.ActivityStreamsReject) error { 169 | a.tr.LogFederatingReject(c, v) 170 | return nil 171 | }, 172 | Add: func(c context.Context, v vocab.ActivityStreamsAdd) error { 173 | a.tr.LogFederatingAdd(c, v) 174 | return nil 175 | }, 176 | Remove: func(c context.Context, v vocab.ActivityStreamsRemove) error { 177 | a.tr.LogFederatingRemove(c, v) 178 | return nil 179 | }, 180 | Like: func(c context.Context, v vocab.ActivityStreamsLike) error { 181 | a.tr.LogFederatingLike(c, v) 182 | return nil 183 | }, 184 | Undo: func(c context.Context, v vocab.ActivityStreamsUndo) error { 185 | a.tr.LogFederatingUndo(c, v) 186 | return nil 187 | }, 188 | Block: func(c context.Context, v vocab.ActivityStreamsBlock) error { 189 | a.tr.LogFederatingBlock(c, v) 190 | return nil 191 | }, 192 | } 193 | return 194 | } 195 | 196 | func (a *Actor) MaxInboxForwardingRecursionDepth(c context.Context) int { 197 | return 3 198 | } 199 | 200 | func (a *Actor) MaxDeliveryRecursionDepth(c context.Context) int { 201 | return 3 202 | } 203 | 204 | func (a *Actor) FilterForwarding(c context.Context, potentialRecipients []*url.URL, activity pub.Activity) (filteredRecipients []*url.URL, err error) { 205 | // Filter out everyone. 206 | a.tr.LogFilterForwarding(c, potentialRecipients, activity, filteredRecipients, err) 207 | return 208 | } 209 | 210 | func (a *Actor) GetInbox(c context.Context, r *http.Request) (p vocab.ActivityStreamsOrderedCollectionPage, err error) { 211 | id := HTTPRequestToIRI(r) 212 | p, err = a.db.GetInbox(c, id) 213 | a.tr.LogGetInbox(c, r, id, p, err) 214 | return 215 | } 216 | 217 | func (a *Actor) PubHandlerFunc(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error) { 218 | c = a.am.AddContextInfo(c, r) 219 | client := &http.Client{ 220 | Timeout: time.Second * 30, 221 | } 222 | // Attempt to verify HTTP Signatures, but only for logging/testing 223 | // purposes. Never deny. 224 | remoteActor, authenticated, httpSigErr := verifyHttpSignatures(c, a.db.hostname, client, r, a.am) 225 | 226 | isASRequest, err = a.h(c, w, r) 227 | if authenticated { 228 | a.tr.LogPubHandlerFuncAuthd(c, r, isASRequest, err, remoteActor, authenticated, httpSigErr) 229 | } else { 230 | a.tr.LogPubHandlerFunc(c, r, isASRequest, err, httpSigErr) 231 | } 232 | return 233 | } 234 | -------------------------------------------------------------------------------- /server/clock.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-fed/activity/pub" 7 | ) 8 | 9 | var _ pub.Clock = &Clock{} 10 | 11 | type Clock struct{} 12 | 13 | func (c *Clock) Now() time.Time { 14 | return time.Now() 15 | } 16 | -------------------------------------------------------------------------------- /server/db.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "sync" 8 | 9 | "github.com/go-fed/activity/pub" 10 | "github.com/go-fed/activity/streams" 11 | "github.com/go-fed/activity/streams/vocab" 12 | ) 13 | 14 | const ( 15 | kContextKeyTestPrefix = "ctxtp" 16 | ) 17 | 18 | var _ pub.Database = &Database{} 19 | 20 | type Database struct { 21 | hostname string 22 | // Database structures 23 | content map[interface{}]vocab.Type 24 | contentFineMu map[interface{}]*sync.Mutex 25 | contentMu sync.RWMutex 26 | } 27 | 28 | func NewDatabase(hostname string) *Database { 29 | return &Database{ 30 | hostname: hostname, 31 | content: make(map[interface{}]vocab.Type, 0), 32 | contentFineMu: make(map[interface{}]*sync.Mutex, 0), 33 | contentMu: sync.RWMutex{}, 34 | } 35 | } 36 | 37 | func (d *Database) Lock(c context.Context, id *url.URL) error { 38 | d.contentMu.RLock() 39 | m, ok := d.contentFineMu[id.String()] 40 | d.contentMu.RUnlock() 41 | if ok { 42 | m.Lock() 43 | } else { 44 | // Not good enough, but we'll cross our fingers. 45 | d.contentMu.Lock() 46 | m = &sync.Mutex{} 47 | d.contentFineMu[id.String()] = m 48 | d.contentMu.Unlock() 49 | m.Lock() 50 | } 51 | return nil 52 | } 53 | 54 | func (d *Database) Unlock(c context.Context, id *url.URL) error { 55 | d.contentMu.RLock() 56 | m, ok := d.contentFineMu[id.String()] 57 | d.contentMu.RUnlock() 58 | if ok { 59 | m.Unlock() 60 | return nil 61 | } else { 62 | return fmt.Errorf("could not find mutex to unlock: %s", id) 63 | } 64 | } 65 | 66 | func (d *Database) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) { 67 | var tr *streams.TypeResolver 68 | tr, err = streams.NewTypeResolver(func(c context.Context, oc vocab.ActivityStreamsOrderedCollection) error { 69 | oi := oc.GetActivityStreamsOrderedItems() 70 | if oi != nil { 71 | for iter := oi.Begin(); iter != oi.End(); iter = iter.Next() { 72 | oid, err := pub.ToId(iter) 73 | if err != nil { 74 | return err 75 | } 76 | if oid.String() == id.String() { 77 | contains = true 78 | return nil 79 | } 80 | } 81 | } 82 | return nil 83 | }) 84 | if err != nil { 85 | return 86 | } 87 | iv, ok := d.content[inbox.String()] 88 | if !ok { 89 | err = fmt.Errorf("no inbox at: %s", inbox) 90 | return 91 | } 92 | err = tr.Resolve(c, iv) 93 | return 94 | } 95 | 96 | func (d *Database) GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { 97 | inbox, err = d.toOCPageFromOC(c, inboxIRI) 98 | return 99 | } 100 | 101 | func (d *Database) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { 102 | err := d.toOCFromOCPage(c, inbox) 103 | return err 104 | } 105 | 106 | func (d *Database) Owns(c context.Context, id *url.URL) (owns bool, err error) { 107 | owns = id.Host == d.hostname 108 | return 109 | } 110 | 111 | func (d *Database) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) { 112 | actorIRI = OutboxIRIToActorIRI(outboxIRI) 113 | return 114 | } 115 | 116 | func (d *Database) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) { 117 | actorIRI = InboxIRIToActorIRI(inboxIRI) 118 | return 119 | } 120 | 121 | func (d *Database) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) { 122 | outboxIRI = OutboxIRIToInboxIRI(inboxIRI) 123 | return 124 | } 125 | 126 | func (d *Database) Exists(c context.Context, id *url.URL) (exists bool, err error) { 127 | _, exists = d.content[id.String()] 128 | return 129 | } 130 | 131 | func (d *Database) Get(c context.Context, id *url.URL) (value vocab.Type, err error) { 132 | var ok bool 133 | value, ok = d.content[id.String()] 134 | if !ok { 135 | err = fmt.Errorf("failed to get by id: %s", id) 136 | } 137 | return 138 | } 139 | 140 | func (d *Database) Create(c context.Context, asType vocab.Type) error { 141 | id, err := pub.GetId(asType) 142 | if err != nil { 143 | return err 144 | } 145 | d.content[id.String()] = asType 146 | return nil 147 | } 148 | 149 | func (d *Database) Update(c context.Context, asType vocab.Type) error { 150 | id, err := pub.GetId(asType) 151 | if err != nil { 152 | return err 153 | } 154 | d.content[id.String()] = asType 155 | return nil 156 | } 157 | 158 | func (d *Database) Delete(c context.Context, id *url.URL) error { 159 | delete(d.content, id.String()) 160 | return nil 161 | } 162 | 163 | func (d *Database) GetOutbox(c context.Context, outboxIRI *url.URL) (outbox vocab.ActivityStreamsOrderedCollectionPage, err error) { 164 | outbox, err = d.toOCPageFromOC(c, outboxIRI) 165 | return 166 | } 167 | 168 | func (d *Database) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { 169 | return d.toOCFromOCPage(c, outbox) 170 | } 171 | 172 | func (d *Database) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { 173 | prefix, ok := c.Value(kContextKeyTestPrefix).(string) 174 | if !ok { 175 | err = fmt.Errorf("cannot determine the test prefix on context for: %s", id) 176 | return 177 | } 178 | idpath := NewIDPath(prefix, t.GetTypeName()) 179 | id = &url.URL{ 180 | Scheme: "https", 181 | Host: d.hostname, 182 | Path: idpath, 183 | } 184 | return 185 | } 186 | 187 | func (d *Database) Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { 188 | followers, err = d.toCollectionFromId(c, ActorIRIToFollowersIRI(actorIRI)) 189 | return 190 | } 191 | 192 | func (d *Database) Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) { 193 | following, err = d.toCollectionFromId(c, ActorIRIToFollowingIRI(actorIRI)) 194 | return 195 | } 196 | 197 | func (d *Database) Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error) { 198 | liked, err = d.toCollectionFromId(c, ActorIRIToLikedIRI(actorIRI)) 199 | return 200 | } 201 | 202 | func (d *Database) toOCPageFromOC(c context.Context, given *url.URL) (result vocab.ActivityStreamsOrderedCollectionPage, err error) { 203 | var tr *streams.TypeResolver 204 | tr, err = streams.NewTypeResolver(func(c context.Context, oc vocab.ActivityStreamsOrderedCollection) error { 205 | result = streams.NewActivityStreamsOrderedCollectionPage() 206 | poi := streams.NewActivityStreamsOrderedItemsProperty() 207 | result.SetActivityStreamsOrderedItems(poi) 208 | // Copy id over to the page 209 | jid := streams.NewJSONLDIdProperty() 210 | jiri, err := pub.GetId(oc) 211 | if err != nil { 212 | return err 213 | } 214 | jid.SetIRI(jiri) 215 | result.SetJSONLDId(jid) 216 | // Copy oi to poi 217 | oi := oc.GetActivityStreamsOrderedItems() 218 | if oi != nil { 219 | for iter := oi.Begin(); iter != oi.End(); iter = iter.Next() { 220 | oid, err := pub.ToId(iter) 221 | if err != nil { 222 | return err 223 | } 224 | poi.AppendIRI(oid) 225 | } 226 | } 227 | return nil 228 | }) 229 | if err != nil { 230 | return 231 | } 232 | iv, ok := d.content[given.String()] 233 | if !ok { 234 | err = fmt.Errorf("no inbox at: %s", given) 235 | return 236 | } 237 | err = tr.Resolve(c, iv) 238 | return 239 | } 240 | 241 | func (d *Database) toOCFromOCPage(c context.Context, page vocab.ActivityStreamsOrderedCollectionPage) error { 242 | tr, err := streams.NewTypeResolver(func(c context.Context, oc vocab.ActivityStreamsOrderedCollection) error { 243 | // Overwrite oc's existing items 244 | oi := streams.NewActivityStreamsOrderedItemsProperty() 245 | oc.SetActivityStreamsOrderedItems(oi) 246 | // Copy poi to oi 247 | poi := page.GetActivityStreamsOrderedItems() 248 | if poi != nil { 249 | for iter := poi.Begin(); iter != poi.End(); iter = iter.Next() { 250 | poid, err := pub.ToId(iter) 251 | if err != nil { 252 | return err 253 | } 254 | oi.AppendIRI(poid) 255 | } 256 | } 257 | return nil 258 | }) 259 | if err != nil { 260 | return err 261 | } 262 | iri, err := pub.GetId(page) 263 | if err != nil { 264 | return err 265 | } 266 | iv, ok := d.content[iri.String()] 267 | if !ok { 268 | return fmt.Errorf("no inbox at: %s", iri) 269 | } 270 | return tr.Resolve(c, iv) 271 | } 272 | 273 | func (d *Database) toCollectionFromId(c context.Context, id *url.URL) (col vocab.ActivityStreamsCollection, err error) { 274 | var tr *streams.TypeResolver 275 | tr, err = streams.NewTypeResolver(func(c context.Context, co vocab.ActivityStreamsCollection) error { 276 | col = co 277 | return nil 278 | }) 279 | if err != nil { 280 | return 281 | } 282 | iv, ok := d.content[id.String()] 283 | if !ok { 284 | err = fmt.Errorf("no inbox at: %s", id) 285 | return 286 | } 287 | err = tr.Resolve(c, iv) 288 | return 289 | 290 | } 291 | -------------------------------------------------------------------------------- /server/mapping.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "fmt" 9 | mathrand "math/rand" 10 | "net/http" 11 | "net/url" 12 | "path" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | kContextKeyRequestPath = "ckrp" 18 | ) 19 | 20 | /* Web mappings */ 21 | 22 | func PathToTestPathPrefix(u *url.URL) (s string, ok bool) { 23 | remain := strings.TrimPrefix( 24 | strings.TrimPrefix(u.Path, kPathPrefixTests), 25 | "/") 26 | var id string 27 | id, ok = testIdFromRemainingPath(remain) 28 | if !ok { 29 | return 30 | } 31 | s = testPathPrefixFromId(id) 32 | return 33 | } 34 | 35 | func StatePathToTestPathPrefix(u *url.URL) (s string, ok bool) { 36 | remain := strings.TrimPrefix( 37 | strings.TrimPrefix(u.Path, kPathTestState), 38 | "/") 39 | var id string 40 | id, ok = testIdFromRemainingPath(remain) 41 | if !ok { 42 | return 43 | } 44 | s = testPathPrefixFromId(id) 45 | return 46 | } 47 | 48 | func InstructionResponsePathToTestPathPrefix(u *url.URL) (s string, ok bool) { 49 | remain := strings.TrimPrefix( 50 | strings.TrimPrefix(u.Path, kPathInstructionResponse), 51 | "/") 52 | var id string 53 | id, ok = testIdFromRemainingPath(remain) 54 | if !ok { 55 | return 56 | } 57 | s = testPathPrefixFromId(id) 58 | return 59 | } 60 | 61 | func InstructionResponsePathToTestState(u *url.URL) (s string, ok bool) { 62 | remain := strings.TrimPrefix( 63 | strings.TrimPrefix(u.Path, kPathInstructionResponse), 64 | "/") 65 | var id string 66 | id, ok = testIdFromRemainingPath(remain) 67 | if !ok { 68 | return 69 | } 70 | s = path.Join(kPathTestState, id) 71 | return 72 | } 73 | 74 | func testIdFromPathPrefix(pathPrefix string) (id string) { 75 | id = strings.TrimPrefix( 76 | strings.TrimPrefix(pathPrefix, kPathPrefixTests), 77 | "/") 78 | return 79 | } 80 | 81 | func testPathPrefixFromId(id string) string { 82 | return path.Join(kPathPrefixTests, id) 83 | } 84 | 85 | func testIdFromRemainingPath(remain string) (s string, ok bool) { 86 | ok = true 87 | parts := strings.Split(remain, "/") 88 | if len(parts) < 1 { 89 | ok = false 90 | return 91 | } 92 | s = parts[0] 93 | return 94 | } 95 | 96 | /* ActivityPub Mappings */ 97 | 98 | type KeyData struct { 99 | PubKeyID string 100 | PubKeyURL string 101 | PrivKey crypto.PrivateKey 102 | } 103 | 104 | type ActorMapping struct { 105 | inboxToKeyData map[string]KeyData 106 | } 107 | 108 | func NewActorMapping() *ActorMapping { 109 | return &ActorMapping{ 110 | inboxToKeyData: make(map[string]KeyData), 111 | } 112 | } 113 | 114 | func (a *ActorMapping) generateKeyData(actor *url.URL) (kd KeyData, err error) { 115 | var rsaKey crypto.PrivateKey 116 | rsaKey, err = rsa.GenerateKey(rand.Reader, 2048) 117 | if err != nil { 118 | return 119 | } 120 | kd = KeyData{ 121 | PubKeyID: "pubKeyFoo", 122 | PubKeyURL: ActorIRIToPubKeyURL(actor).String(), 123 | PrivKey: rsaKey, 124 | } 125 | a.inboxToKeyData[ActorIRIToInboxIRI(actor).Path] = kd 126 | return 127 | } 128 | 129 | func (a *ActorMapping) AddContextInfo(c context.Context, r *http.Request) context.Context { 130 | c = context.WithValue(c, kContextKeyRequestPath, r.URL.Path) 131 | return c 132 | } 133 | 134 | func (a *ActorMapping) GetKeyInfo(c context.Context) (pubKeyID, pubKeyURL string, privKey crypto.PrivateKey, err error) { 135 | path, ok := c.Value(kContextKeyRequestPath).(string) 136 | if !ok { 137 | err = fmt.Errorf("cannot get request path from context in GetKeyInfo") 138 | return 139 | } 140 | kd, ok := a.inboxToKeyData[path] 141 | if !ok { 142 | err = fmt.Errorf("cannot get keydata for %s", path) 143 | return 144 | } 145 | pubKeyID = kd.PubKeyID 146 | pubKeyURL = kd.PubKeyURL 147 | privKey = kd.PrivKey 148 | return 149 | } 150 | 151 | /* Well-known AP endpoints mapping: 152 | Actor: /tests/evaluate/123/actors/ 153 | Inbox: /tests/evaluate/123/actors//inbox 154 | Outbox: /tests/evaluate/123/actors//outbox 155 | Following: /tests/evaluate/123/actors//following 156 | Followers: /tests/evaluate/123/actors//followers 157 | Liked: /tests/evaluate/123/actors//liked 158 | Other: /tests/evaluate/123/other//456 159 | */ 160 | 161 | const ( 162 | kUp = ".." 163 | kOutbox = "outbox" 164 | kInbox = "inbox" 165 | kOther = "other" 166 | kFollowers = "followers" 167 | kFollowing = "following" 168 | kLiked = "liked" 169 | ) 170 | 171 | func OutboxIRIToActorIRI(outbox *url.URL) *url.URL { 172 | c, _ := url.Parse(outbox.String()) 173 | c.Path = path.Clean(path.Join(c.Path, kUp)) 174 | return c 175 | } 176 | 177 | func InboxIRIToActorIRI(inbox *url.URL) *url.URL { 178 | c, _ := url.Parse(inbox.String()) 179 | c.Path = path.Clean(path.Join(c.Path, kUp)) 180 | return c 181 | } 182 | 183 | func OutboxIRIToInboxIRI(outbox *url.URL) *url.URL { 184 | c, _ := url.Parse(outbox.String()) 185 | c.Path = path.Clean(path.Join(c.Path, kUp, kInbox)) 186 | return c 187 | } 188 | 189 | func NewIDPath(pathPrefix string, typename string) string { 190 | testNumber := mathrand.Int() 191 | return path.Join(pathPrefix, kOther, strings.ToLower(typename), fmt.Sprintf("%d", testNumber)) 192 | } 193 | 194 | func NewPathWithIndex(pathPrefix string, typename string, reason string, idx int) string { 195 | return path.Join(pathPrefix, kOther, strings.ToLower(typename), reason, fmt.Sprintf("%d", idx)) 196 | } 197 | 198 | func ActorIRIToInboxIRI(actor *url.URL) *url.URL { 199 | c, _ := url.Parse(actor.String()) 200 | c.Path = path.Join(c.Path, kInbox) 201 | return c 202 | } 203 | 204 | func ActorIRIToOutboxIRI(actor *url.URL) *url.URL { 205 | c, _ := url.Parse(actor.String()) 206 | c.Path = path.Join(c.Path, kOutbox) 207 | return c 208 | } 209 | 210 | func ActorIRIToFollowersIRI(actor *url.URL) *url.URL { 211 | c, _ := url.Parse(actor.String()) 212 | c.Path = path.Join(c.Path, kFollowers) 213 | return c 214 | } 215 | 216 | func ActorIRIToFollowingIRI(actor *url.URL) *url.URL { 217 | c, _ := url.Parse(actor.String()) 218 | c.Path = path.Join(c.Path, kFollowing) 219 | return c 220 | } 221 | 222 | func ActorIRIToLikedIRI(actor *url.URL) *url.URL { 223 | c, _ := url.Parse(actor.String()) 224 | c.Path = path.Join(c.Path, kLiked) 225 | return c 226 | } 227 | 228 | func ActorIRIToPubKeyURL(actor *url.URL) *url.URL { 229 | c, _ := url.Parse(actor.String()) 230 | c.Fragment = "pubKeyFoo" 231 | return c 232 | } 233 | 234 | func IsRelativePathToInboxIRI(path string) bool { 235 | return strings.HasSuffix(path, kInbox) 236 | } 237 | 238 | func IsRelativePathToOutboxIRI(path string) bool { 239 | return strings.HasSuffix(path, kOutbox) 240 | } 241 | 242 | func HTTPRequestToIRI(r *http.Request) *url.URL { 243 | id := &url.URL{} 244 | id.Path = r.URL.Path 245 | id.Host = r.Host 246 | id.Scheme = "https" 247 | return id 248 | } 249 | -------------------------------------------------------------------------------- /server/recorder.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/go-fed/activity/streams" 10 | "github.com/go-fed/activity/streams/vocab" 11 | ) 12 | 13 | type Entry struct { 14 | T time.Time 15 | Msg string 16 | M []byte 17 | } 18 | 19 | func (e Entry) MString() string { 20 | return string(e.M) 21 | } 22 | 23 | type Recorder struct { 24 | entries []Entry 25 | } 26 | 27 | func NewRecorder() *Recorder { 28 | return &Recorder{} 29 | } 30 | 31 | func (r *Recorder) Entries() []Entry { 32 | // TODO: Mutex 33 | return r.entries 34 | } 35 | 36 | func (r *Recorder) Add(msg string, i ...interface{}) { 37 | now := time.Now().UTC() 38 | rec := make([][]byte, len(i)) 39 | for idx, val := range i { 40 | switch t := val.(type) { 41 | case vocab.Type: 42 | m, err := streams.Serialize(t) 43 | if err != nil { 44 | rec[idx] = []byte("could not serialize vocab.Type for logging") 45 | continue 46 | } 47 | b, err := json.MarshalIndent(m, "", " ") 48 | if err != nil { 49 | rec[idx] = []byte(err.Error()) 50 | continue 51 | } 52 | rec[idx] = b 53 | case fmt.Stringer: 54 | rec[idx] = []byte(t.String()) 55 | case error: 56 | rec[idx] = []byte(t.Error()) 57 | default: 58 | rec[idx] = []byte(fmt.Sprintf("%v", t)) 59 | } 60 | } 61 | for idx, b := range rec { 62 | rec[idx] = append([]byte(fmt.Sprintf("[%d] ", idx)), b...) 63 | } 64 | r.entries = append(r.entries, 65 | Entry{ 66 | T: now, 67 | Msg: msg, 68 | M: bytes.Join(rec, []byte("\n")), 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /server/testrunner.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "sync" 9 | 10 | "github.com/go-fed/activity/pub" 11 | "github.com/go-fed/activity/streams/vocab" 12 | ) 13 | 14 | type ServerHandler interface { 15 | Handle(*Instruction) 16 | Update(pending, done []TestInfo, results []Result) 17 | Error(error) 18 | MarkDone() 19 | } 20 | 21 | type TestRunner struct { 22 | raw *Recorder 23 | rawMu sync.RWMutex 24 | tests []Test 25 | completed []Test 26 | results []Result 27 | sh ServerHandler 28 | // Set once test is running 29 | cancel context.CancelFunc 30 | ctx *TestRunnerContext 31 | // Flag bits used for synchronizing AP hook behaviors 32 | hookSyncMu sync.Mutex 33 | awaitFederatedCoreActivity string 34 | awaitFederatedCoreActivityCreate string 35 | awaitFederatedCoreActivityUpdate string 36 | awaitFederatedCoreActivityDelete string 37 | awaitFederatedCoreActivityFollow string 38 | awaitFederatedCoreActivityAccept string 39 | awaitFederatedCoreActivityReject string 40 | awaitFederatedCoreActivityAdd string 41 | awaitFederatedCoreActivityRemove string 42 | awaitFederatedCoreActivityLike string 43 | awaitFederatedCoreActivityBlock string 44 | awaitFederatedCoreActivityUndo string 45 | awaitFederatedCoreActivityMaybeDoubleDelivery string 46 | httpSigsMustMatchRemoteActor bool 47 | } 48 | 49 | func NewTestRunner(sh ServerHandler, tests []Test) *TestRunner { 50 | return &TestRunner{ 51 | tests: tests, 52 | completed: make([]Test, 0, len(tests)), 53 | results: make([]Result, 0, len(tests)), 54 | sh: sh, 55 | } 56 | } 57 | 58 | func (tr *TestRunner) Run(ctx *TestRunnerContext) { 59 | if tr.cancel != nil { 60 | return 61 | } 62 | tr.ctx = ctx 63 | ctx.C, tr.cancel = context.WithCancel(context.Background()) 64 | ctx.APH = tr 65 | go func() { 66 | defer func() { 67 | tr.sh.MarkDone() 68 | }() 69 | var err error 70 | for err == nil && len(tr.tests) > 0 { 71 | err = tr.iterate(ctx) 72 | if err != nil { 73 | tr.sh.Error(err) 74 | return 75 | } else { 76 | tc := make([]TestInfo, len(tr.tests)) 77 | for i, t := range tr.tests { 78 | tc[i] = t.Info() 79 | } 80 | cc := make([]TestInfo, len(tr.completed)) 81 | for i, t := range tr.completed { 82 | cc[i] = t.Info() 83 | } 84 | rc := make([]Result, len(tr.results)) 85 | copy(rc, tr.results) 86 | tr.sh.Update(tc, cc, rc) 87 | } 88 | select { 89 | case <-ctx.C.Done(): 90 | return 91 | default: 92 | // Nothing 93 | } 94 | } 95 | }() 96 | } 97 | 98 | func (tr *TestRunner) iterate(ctx *TestRunnerContext) error { 99 | var i *Instruction 100 | var r *Result 101 | var doneIdx int 102 | for idx, t := range tr.tests { 103 | tr.SetRecorder(t.Recorder()) 104 | if i = t.MaybeGetInstructions(ctx, tr.results); i != nil { 105 | break 106 | } else if r = t.MaybeRunResult(ctx, tr.results); r != nil { 107 | doneIdx = idx 108 | break 109 | } 110 | } 111 | if i != nil { 112 | ctx.PrepInstructionResponse() 113 | tr.sh.Handle(i) 114 | select { 115 | case <-ctx.InstructionCh: 116 | // Remove any previous instructions 117 | tr.sh.Handle(nil) 118 | return nil 119 | case <-ctx.C.Done(): 120 | return nil 121 | } 122 | } 123 | if r != nil { 124 | tr.results = append(tr.results, *r) 125 | tr.completed = append(tr.completed, tr.tests[doneIdx]) 126 | copy(tr.tests[doneIdx:], tr.tests[doneIdx+1:]) 127 | tr.tests[len(tr.tests)-1] = nil 128 | tr.tests = tr.tests[:len(tr.tests)-1] 129 | return nil 130 | } 131 | return fmt.Errorf("Neither an instruction nor result was obtained") 132 | } 133 | 134 | func (tr *TestRunner) Stop() { 135 | if tr.cancel == nil { 136 | return 137 | } 138 | tr.cancel() 139 | tr.cancel = nil 140 | } 141 | 142 | func (tr *TestRunner) ExpectFederatedCoreActivity(keyID string) { 143 | tr.hookSyncMu.Lock() 144 | defer tr.hookSyncMu.Unlock() 145 | tr.awaitFederatedCoreActivity = keyID 146 | } 147 | 148 | func (tr *TestRunner) ExpectFederatedCoreActivityHTTPSigsMustMatchTestRemoteActor(keyID string) { 149 | tr.hookSyncMu.Lock() 150 | defer tr.hookSyncMu.Unlock() 151 | tr.awaitFederatedCoreActivity = keyID 152 | tr.httpSigsMustMatchRemoteActor = true 153 | } 154 | 155 | func (tr *TestRunner) ExpectFederatedCoreActivityCreate(keyID string) { 156 | tr.hookSyncMu.Lock() 157 | defer tr.hookSyncMu.Unlock() 158 | tr.awaitFederatedCoreActivityCreate = keyID 159 | } 160 | 161 | func (tr *TestRunner) ExpectFederatedCoreActivityUpdate(keyID string) { 162 | tr.hookSyncMu.Lock() 163 | defer tr.hookSyncMu.Unlock() 164 | tr.awaitFederatedCoreActivityUpdate = keyID 165 | } 166 | 167 | func (tr *TestRunner) ExpectFederatedCoreActivityDelete(keyID string) { 168 | tr.hookSyncMu.Lock() 169 | defer tr.hookSyncMu.Unlock() 170 | tr.awaitFederatedCoreActivityDelete = keyID 171 | } 172 | 173 | func (tr *TestRunner) ExpectFederatedCoreActivityFollow(keyID string) { 174 | tr.hookSyncMu.Lock() 175 | defer tr.hookSyncMu.Unlock() 176 | tr.awaitFederatedCoreActivityFollow = keyID 177 | } 178 | 179 | func (tr *TestRunner) ExpectFederatedCoreActivityAccept(keyID string) { 180 | tr.hookSyncMu.Lock() 181 | defer tr.hookSyncMu.Unlock() 182 | tr.awaitFederatedCoreActivityAccept = keyID 183 | } 184 | 185 | func (tr *TestRunner) ExpectFederatedCoreActivityReject(keyID string) { 186 | tr.hookSyncMu.Lock() 187 | defer tr.hookSyncMu.Unlock() 188 | tr.awaitFederatedCoreActivityReject = keyID 189 | } 190 | 191 | func (tr *TestRunner) ExpectFederatedCoreActivityAdd(keyID string) { 192 | tr.hookSyncMu.Lock() 193 | defer tr.hookSyncMu.Unlock() 194 | tr.awaitFederatedCoreActivityAdd = keyID 195 | } 196 | 197 | func (tr *TestRunner) ExpectFederatedCoreActivityRemove(keyID string) { 198 | tr.hookSyncMu.Lock() 199 | defer tr.hookSyncMu.Unlock() 200 | tr.awaitFederatedCoreActivityRemove = keyID 201 | } 202 | 203 | func (tr *TestRunner) ExpectFederatedCoreActivityLike(keyID string) { 204 | tr.hookSyncMu.Lock() 205 | defer tr.hookSyncMu.Unlock() 206 | tr.awaitFederatedCoreActivityLike = keyID 207 | } 208 | 209 | func (tr *TestRunner) ExpectFederatedCoreActivityBlock(keyID string) { 210 | tr.hookSyncMu.Lock() 211 | defer tr.hookSyncMu.Unlock() 212 | tr.awaitFederatedCoreActivityBlock = keyID 213 | } 214 | 215 | func (tr *TestRunner) ExpectFederatedCoreActivityUndo(keyID string) { 216 | tr.hookSyncMu.Lock() 217 | defer tr.hookSyncMu.Unlock() 218 | tr.awaitFederatedCoreActivityUndo = keyID 219 | } 220 | 221 | func (tr *TestRunner) ExpectFederatedCoreActivityCheckDoubleDelivery(keyID string) { 222 | tr.hookSyncMu.Lock() 223 | defer tr.hookSyncMu.Unlock() 224 | tr.awaitFederatedCoreActivityMaybeDoubleDelivery = keyID 225 | } 226 | 227 | func (tr *TestRunner) ClearExpectations() { 228 | tr.hookSyncMu.Lock() 229 | defer tr.hookSyncMu.Unlock() 230 | tr.awaitFederatedCoreActivity = "" 231 | tr.awaitFederatedCoreActivityCreate = "" 232 | tr.awaitFederatedCoreActivityUpdate = "" 233 | tr.awaitFederatedCoreActivityDelete = "" 234 | tr.awaitFederatedCoreActivityFollow = "" 235 | tr.awaitFederatedCoreActivityAdd = "" 236 | tr.awaitFederatedCoreActivityRemove = "" 237 | tr.awaitFederatedCoreActivityLike = "" 238 | tr.awaitFederatedCoreActivityBlock = "" 239 | tr.awaitFederatedCoreActivityUndo = "" 240 | tr.awaitFederatedCoreActivityMaybeDoubleDelivery = "" 241 | tr.httpSigsMustMatchRemoteActor = false 242 | } 243 | 244 | func (tr *TestRunner) SetRecorder(r *Recorder) { 245 | tr.rawMu.Lock() 246 | defer tr.rawMu.Unlock() 247 | tr.raw = r 248 | } 249 | 250 | func (tr *TestRunner) Log(msg string, i ...interface{}) { 251 | tr.rawMu.RLock() 252 | defer tr.rawMu.RUnlock() 253 | if tr.raw == nil { 254 | return 255 | } 256 | tr.raw.Add(msg, i...) 257 | } 258 | 259 | func (tr *TestRunner) LogAuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request, authenticated bool, err error) { 260 | tr.Log("LogAuthenticateGetInbox", c, w, r, authenticated, err) 261 | } 262 | 263 | func (tr *TestRunner) LogAuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request, authenticated bool, err error) { 264 | tr.Log("LogAuthenticateGetOutbox", c, w, r, authenticated, err) 265 | } 266 | 267 | func (tr *TestRunner) LogGetOutbox(c context.Context, r *http.Request, outboxId *url.URL, p vocab.ActivityStreamsOrderedCollectionPage, err error) { 268 | tr.Log("LogGetOutbox", c, r, outboxId, p, err) 269 | } 270 | 271 | func (tr *TestRunner) LogNewTransport(c context.Context, actorBoxIRI *url.URL, err error) { 272 | tr.Log("LogNewTransport", c, actorBoxIRI, err) 273 | } 274 | 275 | func (tr *TestRunner) LogDefaultCallback(c context.Context, activity pub.Activity) { 276 | tr.Log("LogDefaultCallback", c, activity) 277 | } 278 | 279 | func (tr *TestRunner) LogPostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) { 280 | tr.Log("LogPostOutboxRequestBodyHook", c, r, data) 281 | } 282 | 283 | func (tr *TestRunner) LogAuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request, authenticated bool, err error) { 284 | tr.Log("LogAuthenticatePostOutbox", c, w, r, authenticated, err) 285 | } 286 | 287 | func (tr *TestRunner) LogSocialCreate(c context.Context, v vocab.ActivityStreamsCreate) { 288 | tr.Log("LogSocialCreate", c, v) 289 | } 290 | 291 | func (tr *TestRunner) LogSocialUpdate(c context.Context, v vocab.ActivityStreamsUpdate) { 292 | tr.Log("LogSocialUpdate", c, v) 293 | } 294 | 295 | func (tr *TestRunner) LogSocialDelete(c context.Context, v vocab.ActivityStreamsDelete) { 296 | tr.Log("LogSocialDelete", c, v) 297 | } 298 | 299 | func (tr *TestRunner) LogSocialFollow(c context.Context, v vocab.ActivityStreamsFollow) { 300 | tr.Log("LogSocialFollow", c, v) 301 | } 302 | 303 | func (tr *TestRunner) LogSocialAdd(c context.Context, v vocab.ActivityStreamsAdd) { 304 | tr.Log("LogSocialAdd", c, v) 305 | } 306 | 307 | func (tr *TestRunner) LogSocialRemove(c context.Context, v vocab.ActivityStreamsRemove) { 308 | tr.Log("LogSocialRemove", c, v) 309 | } 310 | 311 | func (tr *TestRunner) LogSocialLike(c context.Context, v vocab.ActivityStreamsLike) { 312 | tr.Log("LogSocialLike", c, v) 313 | } 314 | 315 | func (tr *TestRunner) LogSocialUndo(c context.Context, v vocab.ActivityStreamsUndo) { 316 | tr.Log("LogSocialUndo", c, v) 317 | } 318 | 319 | func (tr *TestRunner) LogSocialBlock(c context.Context, v vocab.ActivityStreamsBlock) { 320 | tr.Log("LogSocialBlock", c, v) 321 | } 322 | 323 | func (tr *TestRunner) LogPostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) { 324 | tr.Log("LogPostInboxRequestBodyHook", c, r, activity) 325 | tr.hookSyncMu.Lock() 326 | defer tr.hookSyncMu.Unlock() 327 | if len(tr.awaitFederatedCoreActivityMaybeDoubleDelivery) > 0 { 328 | tr.Log("Checking double-delivery condition") 329 | iri, err := pub.GetId(activity) 330 | if err != nil { 331 | tr.Log("Error attempting to get the id of the activity", err) 332 | return 333 | } 334 | key := tr.awaitFederatedCoreActivityMaybeDoubleDelivery 335 | preIRI, err := getInstructionResponseAsDirectIRI(tr.ctx, key) 336 | if err != nil { 337 | tr.Log("First time seeing activity with id, because error returned", iri, err) 338 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 339 | } else if preIRI.String() == iri.String() { 340 | tr.Log("Second time seeing activity with the same id", iri) 341 | tr.awaitFederatedCoreActivityMaybeDoubleDelivery = "" 342 | tr.ctx.C = context.WithValue(tr.ctx.C, key, []*url.URL{preIRI, iri}) 343 | } else { 344 | tr.Log("Second time seeing activity, with different ids", iri, preIRI) 345 | tr.awaitFederatedCoreActivityMaybeDoubleDelivery = "" 346 | tr.ctx.C = context.WithValue(tr.ctx.C, key, []*url.URL{preIRI, iri}) 347 | } 348 | tr.ctx.InstructionDone() 349 | } 350 | } 351 | 352 | func (tr *TestRunner) LogAuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request, remoteActor *url.URL, authenticated bool, err error) { 353 | tr.Log("LogAuthenticatePostInbox", c, w, r, remoteActor, authenticated, err) 354 | } 355 | 356 | func (tr *TestRunner) LogBlocked(c context.Context, actorIRIs []*url.URL, blocked bool, err error) { 357 | tr.Log("LogBlocked", c, actorIRIs, blocked, err) 358 | } 359 | 360 | func (tr *TestRunner) LogFederatingCreate(c context.Context, v vocab.ActivityStreamsCreate) { 361 | tr.Log("LogFederatingCreate", c, v) 362 | iri, err := pub.GetId(v) 363 | if err != nil { 364 | tr.Log("Could not get Create iri: " + err.Error()) 365 | return 366 | } 367 | tr.hookSyncMu.Lock() 368 | defer tr.hookSyncMu.Unlock() 369 | if len(tr.awaitFederatedCoreActivity) > 0 { 370 | key := tr.awaitFederatedCoreActivity 371 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 372 | tr.awaitFederatedCoreActivity = "" 373 | tr.ctx.InstructionDone() 374 | } 375 | if len(tr.awaitFederatedCoreActivityCreate) > 0 { 376 | key := tr.awaitFederatedCoreActivityCreate 377 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 378 | tr.awaitFederatedCoreActivityCreate = "" 379 | tr.ctx.InstructionDone() 380 | } 381 | } 382 | 383 | func (tr *TestRunner) LogFederatingUpdate(c context.Context, v vocab.ActivityStreamsUpdate) { 384 | tr.Log("LogFederatingUpdate", c, v) 385 | iri, err := pub.GetId(v) 386 | if err != nil { 387 | tr.Log("Could not get Update iri: " + err.Error()) 388 | return 389 | } 390 | tr.hookSyncMu.Lock() 391 | defer tr.hookSyncMu.Unlock() 392 | if len(tr.awaitFederatedCoreActivity) > 0 { 393 | key := tr.awaitFederatedCoreActivity 394 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 395 | tr.awaitFederatedCoreActivity = "" 396 | tr.ctx.InstructionDone() 397 | } 398 | if len(tr.awaitFederatedCoreActivityUpdate) > 0 { 399 | key := tr.awaitFederatedCoreActivityUpdate 400 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 401 | tr.awaitFederatedCoreActivityUpdate = "" 402 | tr.ctx.InstructionDone() 403 | } 404 | } 405 | 406 | func (tr *TestRunner) LogFederatingDelete(c context.Context, v vocab.ActivityStreamsDelete) { 407 | tr.Log("LogFederatingDelete", c, v) 408 | iri, err := pub.GetId(v) 409 | if err != nil { 410 | tr.Log("Could not get Delete iri: " + err.Error()) 411 | return 412 | } 413 | tr.hookSyncMu.Lock() 414 | defer tr.hookSyncMu.Unlock() 415 | if len(tr.awaitFederatedCoreActivity) > 0 { 416 | key := tr.awaitFederatedCoreActivity 417 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 418 | tr.awaitFederatedCoreActivity = "" 419 | tr.ctx.InstructionDone() 420 | } 421 | if len(tr.awaitFederatedCoreActivityDelete) > 0 { 422 | key := tr.awaitFederatedCoreActivityDelete 423 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 424 | tr.awaitFederatedCoreActivityDelete = "" 425 | tr.ctx.InstructionDone() 426 | } 427 | } 428 | 429 | func (tr *TestRunner) LogFederatingFollow(c context.Context, v vocab.ActivityStreamsFollow) { 430 | tr.Log("LogFederatingFollow", c, v) 431 | iri, err := pub.GetId(v) 432 | if err != nil { 433 | tr.Log("Could not get Follow iri: " + err.Error()) 434 | return 435 | } 436 | tr.hookSyncMu.Lock() 437 | defer tr.hookSyncMu.Unlock() 438 | if len(tr.awaitFederatedCoreActivity) > 0 { 439 | key := tr.awaitFederatedCoreActivity 440 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 441 | tr.awaitFederatedCoreActivity = "" 442 | tr.ctx.InstructionDone() 443 | } 444 | if len(tr.awaitFederatedCoreActivityFollow) > 0 { 445 | key := tr.awaitFederatedCoreActivityFollow 446 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 447 | tr.awaitFederatedCoreActivityFollow = "" 448 | tr.ctx.InstructionDone() 449 | } 450 | } 451 | 452 | func (tr *TestRunner) LogFederatingAccept(c context.Context, v vocab.ActivityStreamsAccept) { 453 | tr.Log("LogFederatingAccept", c, v) 454 | iri, err := pub.GetId(v) 455 | if err != nil { 456 | tr.Log("Could not get Accept iri: " + err.Error()) 457 | return 458 | } 459 | tr.hookSyncMu.Lock() 460 | defer tr.hookSyncMu.Unlock() 461 | if len(tr.awaitFederatedCoreActivity) > 0 { 462 | key := tr.awaitFederatedCoreActivity 463 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 464 | tr.awaitFederatedCoreActivity = "" 465 | tr.ctx.InstructionDone() 466 | } 467 | if len(tr.awaitFederatedCoreActivityAccept) > 0 { 468 | key := tr.awaitFederatedCoreActivityAccept 469 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 470 | tr.awaitFederatedCoreActivityAccept = "" 471 | tr.ctx.InstructionDone() 472 | } 473 | } 474 | 475 | func (tr *TestRunner) LogFederatingReject(c context.Context, v vocab.ActivityStreamsReject) { 476 | tr.Log("LogFederatingReject", c, v) 477 | iri, err := pub.GetId(v) 478 | if err != nil { 479 | tr.Log("Could not get Reject iri: " + err.Error()) 480 | return 481 | } 482 | tr.hookSyncMu.Lock() 483 | defer tr.hookSyncMu.Unlock() 484 | if len(tr.awaitFederatedCoreActivity) > 0 { 485 | key := tr.awaitFederatedCoreActivity 486 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 487 | tr.awaitFederatedCoreActivity = "" 488 | tr.ctx.InstructionDone() 489 | } 490 | if len(tr.awaitFederatedCoreActivityReject) > 0 { 491 | key := tr.awaitFederatedCoreActivityReject 492 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 493 | tr.awaitFederatedCoreActivityReject = "" 494 | tr.ctx.InstructionDone() 495 | } 496 | } 497 | 498 | func (tr *TestRunner) LogFederatingAdd(c context.Context, v vocab.ActivityStreamsAdd) { 499 | tr.Log("LogFederatingAdd", c, v) 500 | iri, err := pub.GetId(v) 501 | if err != nil { 502 | tr.Log("Could not get Add iri: " + err.Error()) 503 | return 504 | } 505 | tr.hookSyncMu.Lock() 506 | defer tr.hookSyncMu.Unlock() 507 | if len(tr.awaitFederatedCoreActivity) > 0 { 508 | key := tr.awaitFederatedCoreActivity 509 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 510 | tr.awaitFederatedCoreActivity = "" 511 | tr.ctx.InstructionDone() 512 | } 513 | if len(tr.awaitFederatedCoreActivityAdd) > 0 { 514 | key := tr.awaitFederatedCoreActivityAdd 515 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 516 | tr.awaitFederatedCoreActivityAdd = "" 517 | tr.ctx.InstructionDone() 518 | } 519 | } 520 | 521 | func (tr *TestRunner) LogFederatingRemove(c context.Context, v vocab.ActivityStreamsRemove) { 522 | tr.Log("LogFederatingRemove", c, v) 523 | iri, err := pub.GetId(v) 524 | if err != nil { 525 | tr.Log("Could not get Remove iri: " + err.Error()) 526 | return 527 | } 528 | tr.hookSyncMu.Lock() 529 | defer tr.hookSyncMu.Unlock() 530 | if len(tr.awaitFederatedCoreActivity) > 0 { 531 | key := tr.awaitFederatedCoreActivity 532 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 533 | tr.awaitFederatedCoreActivity = "" 534 | tr.ctx.InstructionDone() 535 | } 536 | if len(tr.awaitFederatedCoreActivityRemove) > 0 { 537 | key := tr.awaitFederatedCoreActivityRemove 538 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 539 | tr.awaitFederatedCoreActivityRemove = "" 540 | tr.ctx.InstructionDone() 541 | } 542 | } 543 | 544 | func (tr *TestRunner) LogFederatingLike(c context.Context, v vocab.ActivityStreamsLike) { 545 | tr.Log("LogFederatingLike", c, v) 546 | iri, err := pub.GetId(v) 547 | if err != nil { 548 | tr.Log("Could not get Like iri: " + err.Error()) 549 | return 550 | } 551 | tr.hookSyncMu.Lock() 552 | defer tr.hookSyncMu.Unlock() 553 | if len(tr.awaitFederatedCoreActivity) > 0 { 554 | key := tr.awaitFederatedCoreActivity 555 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 556 | tr.awaitFederatedCoreActivity = "" 557 | tr.ctx.InstructionDone() 558 | } 559 | if len(tr.awaitFederatedCoreActivityLike) > 0 { 560 | key := tr.awaitFederatedCoreActivityLike 561 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 562 | tr.awaitFederatedCoreActivityLike = "" 563 | tr.ctx.InstructionDone() 564 | } 565 | } 566 | 567 | func (tr *TestRunner) LogFederatingUndo(c context.Context, v vocab.ActivityStreamsUndo) { 568 | tr.Log("LogFederatingUndo", c, v) 569 | iri, err := pub.GetId(v) 570 | if err != nil { 571 | tr.Log("Could not get Undo iri: " + err.Error()) 572 | return 573 | } 574 | tr.hookSyncMu.Lock() 575 | defer tr.hookSyncMu.Unlock() 576 | if len(tr.awaitFederatedCoreActivity) > 0 { 577 | key := tr.awaitFederatedCoreActivity 578 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 579 | tr.awaitFederatedCoreActivity = "" 580 | tr.ctx.InstructionDone() 581 | } 582 | if len(tr.awaitFederatedCoreActivityUndo) > 0 { 583 | key := tr.awaitFederatedCoreActivityUndo 584 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 585 | tr.awaitFederatedCoreActivityUndo = "" 586 | tr.ctx.InstructionDone() 587 | } 588 | } 589 | 590 | func (tr *TestRunner) LogFederatingBlock(c context.Context, v vocab.ActivityStreamsBlock) { 591 | tr.Log("LogFederatingBlock", c, v) 592 | iri, err := pub.GetId(v) 593 | if err != nil { 594 | tr.Log("Could not get Block iri: " + err.Error()) 595 | return 596 | } 597 | tr.hookSyncMu.Lock() 598 | defer tr.hookSyncMu.Unlock() 599 | if len(tr.awaitFederatedCoreActivity) > 0 { 600 | key := tr.awaitFederatedCoreActivity 601 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 602 | tr.awaitFederatedCoreActivity = "" 603 | tr.ctx.InstructionDone() 604 | } 605 | if len(tr.awaitFederatedCoreActivityBlock) > 0 { 606 | key := tr.awaitFederatedCoreActivityBlock 607 | tr.ctx.C = context.WithValue(tr.ctx.C, key, iri) 608 | tr.awaitFederatedCoreActivityBlock = "" 609 | tr.ctx.InstructionDone() 610 | } 611 | } 612 | 613 | func (tr *TestRunner) LogFilterForwarding(c context.Context, potentialRecipients []*url.URL, activity pub.Activity, filteredRecipients []*url.URL, err error) { 614 | tr.Log("LogFilterForwarding", c, potentialRecipients, activity, filteredRecipients, err) 615 | } 616 | 617 | func (tr *TestRunner) LogGetInbox(c context.Context, r *http.Request, outboxId *url.URL, p vocab.ActivityStreamsOrderedCollectionPage, err error) { 618 | tr.Log("LogGetInbox", c, r, outboxId, p, err) 619 | } 620 | 621 | func (tr *TestRunner) LogPubHandlerFuncAuthd(c context.Context, r *http.Request, isASRequest bool, err error, remoteActor *url.URL, authenticated bool, httpSigErr error) { 622 | tr.Log("LogHandle Web Request (with HTTP Signature)", c, r, isASRequest, err, remoteActor, authenticated, httpSigErr) 623 | tr.hookSyncMu.Lock() 624 | defer tr.hookSyncMu.Unlock() 625 | if tr.httpSigsMustMatchRemoteActor { 626 | matched := tr.ctx.TestRemoteActorID.String() == remoteActor.String() 627 | tr.ctx.C = context.WithValue(tr.ctx.C, kHttpSigMatchRemoteActorKeyId, matched) 628 | tr.httpSigsMustMatchRemoteActor = false 629 | } 630 | return 631 | } 632 | func (tr *TestRunner) LogPubHandlerFunc(c context.Context, r *http.Request, isASRequest bool, err, httpSigErr error) { 633 | tr.Log("LogHandle Web Request (no HTTP Signatures)", c, r, isASRequest, err, httpSigErr) 634 | tr.hookSyncMu.Lock() 635 | defer tr.hookSyncMu.Unlock() 636 | if tr.httpSigsMustMatchRemoteActor { 637 | tr.ctx.C = context.WithValue(tr.ctx.C, kHttpSigMatchRemoteActorKeyId, false) 638 | tr.httpSigsMustMatchRemoteActor = false 639 | } 640 | return 641 | } 642 | -------------------------------------------------------------------------------- /server/testserver.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/go-fed/activity/pub" 17 | "github.com/go-fed/activity/streams" 18 | ) 19 | 20 | const ( 21 | kRecurLimit = 1000 22 | ) 23 | 24 | type testBundle struct { 25 | started time.Time 26 | enableWebfinger bool 27 | tr *TestRunner 28 | am *ActorMapping 29 | pfa pub.FederatingActor 30 | ctx *TestRunnerContext 31 | db *Database 32 | handler pub.HandlerFunc 33 | state *TestState 34 | stateMu *sync.RWMutex 35 | timer *time.Timer 36 | } 37 | 38 | type TestState struct { 39 | ID string 40 | I Instruction 41 | Pending []TestInfo 42 | Completed []TestInfo 43 | Results []Result 44 | Err error 45 | Done bool 46 | } 47 | 48 | func (t *TestState) nState(state TestResultState) int { 49 | n := 0 50 | for _, r := range t.Results { 51 | if r.State == state { 52 | n++ 53 | } 54 | } 55 | return n 56 | } 57 | 58 | func (t TestState) NPass() int { 59 | return t.nState(TestResultPass) 60 | } 61 | 62 | func (t TestState) NFail() int { 63 | return t.nState(TestResultFail) 64 | } 65 | 66 | func (t TestState) NInconclusive() int { 67 | return t.nState(TestResultInconclusive) 68 | } 69 | 70 | func (t *TestState) Clone() *TestState { 71 | ts := &TestState{ 72 | Pending: make([]TestInfo, len(t.Pending)), 73 | Completed: make([]TestInfo, len(t.Completed)), 74 | Results: make([]Result, len(t.Results)), 75 | } 76 | copy(ts.Pending, t.Pending) 77 | copy(ts.Completed, t.Completed) 78 | copy(ts.Results, t.Results) 79 | ts.ID = t.ID 80 | ts.I = t.I 81 | ts.Err = t.Err 82 | ts.Done = t.Done 83 | return ts 84 | } 85 | 86 | type TestServer struct { 87 | hostname string 88 | pathParent string 89 | testTimeout time.Duration 90 | maxTests int 91 | // Async members 92 | cache map[string]testBundle 93 | cacheMu sync.RWMutex 94 | } 95 | 96 | func NewTestServer(hostname, pathParent string, timeout time.Duration, max int) *TestServer { 97 | return &TestServer{ 98 | hostname: hostname, 99 | pathParent: pathParent, 100 | testTimeout: timeout, 101 | maxTests: max, 102 | cache: make(map[string]testBundle, 0), 103 | cacheMu: sync.RWMutex{}, 104 | } 105 | } 106 | 107 | func (ts *TestServer) StartTest(c context.Context, pathPrefix string, c2s, s2s, enableWebfinger bool, maxDeliverRecur int, testRemoteActorID *url.URL) error { 108 | started := time.Now().UTC() 109 | tb := ts.newTestBundle( 110 | pathPrefix, 111 | c2s, 112 | s2s, 113 | enableWebfinger, 114 | started, 115 | testRemoteActorID) 116 | 117 | ok := true 118 | ts.cacheMu.Lock() 119 | if len(ts.cache) >= ts.maxTests { 120 | ok = false 121 | } else { 122 | ts.cache[pathPrefix] = tb 123 | } 124 | ts.cacheMu.Unlock() 125 | if !ok { 126 | return fmt.Errorf("Too many tests ongoing. Please try again in %s.", ts.testTimeout) 127 | } 128 | 129 | // Prepare our test actor information. 130 | var err error 131 | tb.ctx.TestActor0, err = ts.prepareActor(c, tb, pathPrefix, kActor0) 132 | if err != nil { 133 | return err 134 | } 135 | tb.ctx.TestActor1, err = ts.prepareActor(c, tb, pathPrefix, kActor1) 136 | if err != nil { 137 | return err 138 | } 139 | tb.ctx.TestActor2, err = ts.prepareActor(c, tb, pathPrefix, kActor2) 140 | if err != nil { 141 | return err 142 | } 143 | tb.ctx.TestActor3, err = ts.prepareActor(c, tb, pathPrefix, kActor3) 144 | if err != nil { 145 | return err 146 | } 147 | tb.ctx.TestActor4, err = ts.prepareActor(c, tb, pathPrefix, kActor4) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | if s2s { 153 | // Prepare nested collections of actors for testing dereference 154 | // limits during delivery. 155 | if maxDeliverRecur < 1 { 156 | return fmt.Errorf("maximum recursion limit for delivery must be >= 1") 157 | } else if maxDeliverRecur > kRecurLimit { 158 | maxDeliverRecur = kRecurLimit 159 | tb.ctx.RecurLimitExceeded = true 160 | } 161 | // Iterate from deepest to shallowest collection. The extra 162 | // collection ensures the 0th one will not be fetched. 163 | // 164 | // Set up a series of nested Collections whose members are: 165 | // {kActor1, kActor2, {kActor3, {...{kActor4}...}}} 166 | var prevIRI *url.URL 167 | max := maxDeliverRecur + 1 168 | var addedActorsLevel2 bool 169 | for i := 0; i < max; i++ { 170 | col := streams.NewActivityStreamsCollection() 171 | id := streams.NewJSONLDIdProperty() 172 | idIRI := &url.URL{ 173 | Scheme: "https", 174 | Host: ts.hostname, 175 | Path: NewPathWithIndex(pathPrefix, col.GetTypeName(), "nested", max-i), 176 | } 177 | id.Set(idIRI) 178 | col.SetJSONLDId(id) 179 | 180 | items := streams.NewActivityStreamsItemsProperty() 181 | if i == 0 { 182 | items.AppendIRI(tb.ctx.TestActor4.ActivityPubIRI) 183 | } 184 | if i >= maxDeliverRecur-1 && !addedActorsLevel2 { 185 | items.AppendIRI(tb.ctx.TestActor3.ActivityPubIRI) 186 | addedActorsLevel2 = true // Depending on depth, could happen twice. 187 | } 188 | if i >= maxDeliverRecur { 189 | items.AppendIRI(tb.ctx.TestActor2.ActivityPubIRI) 190 | items.AppendIRI(tb.ctx.TestActor1.ActivityPubIRI) 191 | } 192 | if i > 0 { 193 | items.AppendIRI(prevIRI) 194 | } 195 | prevIRI = idIRI 196 | col.SetActivityStreamsItems(items) 197 | 198 | if err = tb.db.Create(c, col); err != nil { 199 | return err 200 | } 201 | } 202 | testID := testIdFromPathPrefix(pathPrefix) 203 | tb.ctx.RootRecurCollectionID = deliverableIDs{ 204 | ActivityPubIRI: prevIRI, 205 | WebfingerId: fmt.Sprintf("@%s%s%s@%s", kRecurCollection, kWebfingerTestDelim, testID, ts.hostname), 206 | WebfingerSubject: fmt.Sprintf("%s%s%s", kRecurCollection, kWebfingerTestDelim, testID), 207 | } 208 | } 209 | 210 | tb.timer = time.AfterFunc(ts.testTimeout, func() { 211 | tb.tr.Stop() 212 | ts.cacheMu.Lock() 213 | delete(ts.cache, pathPrefix) 214 | ts.cacheMu.Unlock() 215 | }) 216 | tb.tr.Run(tb.ctx) 217 | return nil 218 | } 219 | 220 | func (ts *TestServer) HandleWeb(c context.Context, w http.ResponseWriter, r *http.Request) { 221 | if !strings.HasPrefix(r.URL.Path, ts.pathParent) { 222 | http.Error(w, fmt.Sprintf("Cannot HandleWeb for path %s", r.URL.Path), http.StatusInternalServerError) 223 | return 224 | } 225 | remain := strings.TrimPrefix( 226 | strings.TrimPrefix(r.URL.Path, ts.pathParent), 227 | "/") 228 | parts := strings.Split(remain, "/") 229 | if len(parts) < 2 { 230 | http.Error(w, fmt.Sprintf("Cannot HandleWeb for path %s", r.URL.Path), http.StatusInternalServerError) 231 | return 232 | } 233 | testID := parts[0] 234 | testPathPrefix := path.Join(ts.pathParent, testID) 235 | 236 | ts.cacheMu.RLock() 237 | tb, ok := ts.cache[testPathPrefix] 238 | ts.cacheMu.RUnlock() 239 | if !ok { 240 | http.NotFound(w, r) 241 | return 242 | } 243 | relPathPrefix := path.Join(testPathPrefix, parts[1]) 244 | restPath := strings.TrimPrefix(r.URL.Path, relPathPrefix) 245 | if IsRelativePathToInboxIRI(restPath) { 246 | if isAP, err := tb.pfa.GetInbox(c, w, r); err != nil { 247 | http.Error(w, err.Error(), http.StatusInternalServerError) 248 | } else if isAP { 249 | // Nothing, success! 250 | } else if isAP, err = tb.pfa.PostInbox(c, w, r); err != nil { 251 | http.Error(w, err.Error(), http.StatusInternalServerError) 252 | } else if isAP { 253 | // Nothing, success! 254 | } else { 255 | http.Error(w, "Not an ActivityPub request to the Inbox", http.StatusBadRequest) 256 | } 257 | } else if IsRelativePathToOutboxIRI(restPath) { 258 | if isAP, err := tb.pfa.GetOutbox(c, w, r); err != nil { 259 | http.Error(w, err.Error(), http.StatusInternalServerError) 260 | } else if isAP { 261 | // Nothing, success! 262 | } else if isAP, err = tb.pfa.PostOutbox(c, w, r); err != nil { 263 | http.Error(w, err.Error(), http.StatusInternalServerError) 264 | } else if isAP { 265 | // Nothing, success! 266 | } else { 267 | http.Error(w, "Not an ActivityPub request to the Outbox", http.StatusBadRequest) 268 | } 269 | } else { 270 | if isAP, err := tb.handler(c, w, r); err != nil { 271 | http.Error(w, err.Error(), http.StatusInternalServerError) 272 | } else if isAP { 273 | // Nothing, success! 274 | } else { 275 | http.Error(w, "Not an ActivityPub request", http.StatusBadRequest) 276 | } 277 | } 278 | } 279 | 280 | func (ts *TestServer) TestState(pathPrefix string) (t TestState, ok bool) { 281 | var tb testBundle 282 | ts.cacheMu.RLock() 283 | tb, ok = ts.cache[pathPrefix] 284 | ts.cacheMu.RUnlock() 285 | if !ok { 286 | return 287 | } 288 | tb.stateMu.RLock() 289 | if ok { 290 | t = *tb.state.Clone() 291 | } 292 | tb.stateMu.RUnlock() 293 | return 294 | } 295 | 296 | func (ts *TestServer) HandleInstructionResponse(pathPrefix string, vals map[string][]string) { 297 | ts.cacheMu.RLock() 298 | tb, ok := ts.cache[pathPrefix] 299 | ts.cacheMu.RUnlock() 300 | if !ok { 301 | return 302 | } 303 | tb.stateMu.RLock() 304 | if ok { 305 | for k, v := range vals { 306 | tb.ctx.C = context.WithValue(tb.ctx.C, k, v) 307 | } 308 | tb.ctx.InstructionDone() 309 | } 310 | tb.stateMu.RUnlock() 311 | } 312 | 313 | func (ts *TestServer) HandleWebfinger(pathPrefix string, user string) (username string, apIRI *url.URL, err error) { 314 | ts.cacheMu.RLock() 315 | tb, ok := ts.cache[pathPrefix] 316 | ts.cacheMu.RUnlock() 317 | if !ok { 318 | err = fmt.Errorf("test not found for: %s", pathPrefix) 319 | return 320 | } 321 | if !tb.enableWebfinger { 322 | err = fmt.Errorf("webfinger is not enabled for this test") 323 | return 324 | } 325 | switch user { 326 | case kActor0: 327 | apIRI = tb.ctx.TestActor0.ActivityPubIRI 328 | username = tb.ctx.TestActor0.WebfingerSubject 329 | case kActor1: 330 | apIRI = tb.ctx.TestActor1.ActivityPubIRI 331 | username = tb.ctx.TestActor1.WebfingerSubject 332 | case kActor2: 333 | apIRI = tb.ctx.TestActor2.ActivityPubIRI 334 | username = tb.ctx.TestActor2.WebfingerSubject 335 | case kActor3: 336 | apIRI = tb.ctx.TestActor3.ActivityPubIRI 337 | username = tb.ctx.TestActor3.WebfingerSubject 338 | case kActor4: 339 | apIRI = tb.ctx.TestActor4.ActivityPubIRI 340 | username = tb.ctx.TestActor4.WebfingerSubject 341 | case kRecurCollection: 342 | apIRI = tb.ctx.RootRecurCollectionID.ActivityPubIRI 343 | username = tb.ctx.RootRecurCollectionID.WebfingerSubject 344 | default: 345 | err = fmt.Errorf("no webfinger for user with name %s", user) 346 | } 347 | return 348 | } 349 | 350 | func (ts *TestServer) shutdown() { 351 | ts.cacheMu.Lock() 352 | for _, v := range ts.cache { 353 | v.timer.Stop() 354 | v.tr.Stop() 355 | } 356 | ts.cacheMu.Unlock() 357 | } 358 | 359 | func (ts *TestServer) newTestBundle(pathPrefix string, c2s, s2s, enableWebfinger bool, started time.Time, testRemoteActorID *url.URL) testBundle { 360 | tests := newCommonTests() 361 | if c2s { 362 | tests = append(tests, newSocialTests()...) 363 | } 364 | if s2s { 365 | tests = append(tests, newFederatingTests()...) 366 | } 367 | db := NewDatabase(ts.hostname) 368 | am := NewActorMapping() 369 | tsc := &TestServerClosure{ 370 | ts, 371 | pathPrefix, 372 | } 373 | tr := NewTestRunner(tsc, tests) 374 | clock := &Clock{} 375 | handler := pub.NewActivityStreamsHandler(db, clock) 376 | actor := NewActor(db, am, tr, handler) 377 | pfa := pub.NewActor(actor, actor, actor, db, clock) 378 | ctx := &TestRunnerContext{ 379 | TestRemoteActorID: testRemoteActorID, 380 | Actor: pfa, 381 | Transporter: actor, 382 | DB: db, 383 | AM: am, 384 | } 385 | return testBundle{ 386 | started: started, 387 | enableWebfinger: enableWebfinger, 388 | tr: tr, 389 | am: am, 390 | pfa: pfa, 391 | ctx: ctx, 392 | db: db, 393 | state: &TestState{ 394 | ID: testIdFromPathPrefix(pathPrefix), 395 | }, 396 | handler: actor.PubHandlerFunc, 397 | stateMu: &sync.RWMutex{}, 398 | } 399 | } 400 | 401 | const ( 402 | kActor0 = "alex" 403 | kActor1 = "taylor" 404 | kActor2 = "logan" 405 | kActor3 = "austin" 406 | kActor4 = "peyton" 407 | kRecurCollection = "recursiveCollection" 408 | ) 409 | 410 | func (ts *TestServer) prepareActor(c context.Context, tb testBundle, prefix, name string) (actor actorIDs, err error) { 411 | testID := testIdFromPathPrefix(prefix) 412 | actor = actorIDs{ 413 | ActivityPubIRI: &url.URL{ 414 | Scheme: "https", 415 | Host: ts.hostname, 416 | Path: path.Join(prefix, name), 417 | }, 418 | WebfingerId: fmt.Sprintf("@%s%s%s@%s", name, kWebfingerTestDelim, testID, ts.hostname), 419 | WebfingerSubject: fmt.Sprintf("%s%s%s", name, kWebfingerTestDelim, testID), 420 | } 421 | actorIRI := actor.ActivityPubIRI 422 | 423 | var kd KeyData 424 | kd, err = tb.am.generateKeyData(actorIRI) 425 | if err != nil { 426 | return 427 | } 428 | 429 | person := streams.NewActivityStreamsPerson() 430 | id := streams.NewJSONLDIdProperty() 431 | id.Set(actorIRI) 432 | person.SetJSONLDId(id) 433 | 434 | urlProp := streams.NewActivityStreamsUrlProperty() 435 | urlProp.AppendIRI(actorIRI) 436 | person.SetActivityStreamsUrl(urlProp) 437 | 438 | inboxIRI := ActorIRIToInboxIRI(actorIRI) 439 | inbox := streams.NewActivityStreamsInboxProperty() 440 | inbox.SetIRI(inboxIRI) 441 | person.SetActivityStreamsInbox(inbox) 442 | 443 | outboxIRI := ActorIRIToOutboxIRI(actorIRI) 444 | outbox := streams.NewActivityStreamsOutboxProperty() 445 | outbox.SetIRI(outboxIRI) 446 | person.SetActivityStreamsOutbox(outbox) 447 | 448 | followersIRI := ActorIRIToFollowersIRI(actorIRI) 449 | followers := streams.NewActivityStreamsFollowersProperty() 450 | followers.SetIRI(followersIRI) 451 | person.SetActivityStreamsFollowers(followers) 452 | 453 | followingIRI := ActorIRIToFollowingIRI(actorIRI) 454 | following := streams.NewActivityStreamsFollowingProperty() 455 | following.SetIRI(followingIRI) 456 | person.SetActivityStreamsFollowing(following) 457 | 458 | likedIRI := ActorIRIToLikedIRI(actorIRI) 459 | liked := streams.NewActivityStreamsLikedProperty() 460 | liked.SetIRI(likedIRI) 461 | person.SetActivityStreamsLiked(liked) 462 | 463 | nameProp := streams.NewActivityStreamsNameProperty() 464 | nameProp.AppendXMLSchemaString(name) 465 | person.SetActivityStreamsName(nameProp) 466 | 467 | preferredUsername := streams.NewActivityStreamsPreferredUsernameProperty() 468 | preferredUsername.SetXMLSchemaString(name) 469 | person.SetActivityStreamsPreferredUsername(preferredUsername) 470 | 471 | summary := streams.NewActivityStreamsSummaryProperty() 472 | summary.AppendXMLSchemaString("This is a test user, " + name) 473 | person.SetActivityStreamsSummary(summary) 474 | 475 | var pubPkix []byte 476 | pubPkix, err = x509.MarshalPKIXPublicKey(&kd.PrivKey.(*rsa.PrivateKey).PublicKey) 477 | if err != nil { 478 | return 479 | } 480 | pubBytes := pem.EncodeToMemory(&pem.Block{ 481 | Type: "PUBLIC KEY", 482 | Bytes: pubPkix, 483 | }) 484 | pubString := string(pubBytes) 485 | 486 | pubk := streams.NewW3IDSecurityV1PublicKey() 487 | var pubkIRI *url.URL 488 | pubkIRI, err = url.Parse(kd.PubKeyURL) 489 | if err != nil { 490 | return 491 | } 492 | pubkID := streams.NewJSONLDIdProperty() 493 | pubkID.Set(pubkIRI) 494 | pubk.SetJSONLDId(pubkID) 495 | 496 | owner := streams.NewW3IDSecurityV1OwnerProperty() 497 | owner.SetIRI(actorIRI) 498 | pubk.SetW3IDSecurityV1Owner(owner) 499 | 500 | keyPem := streams.NewW3IDSecurityV1PublicKeyPemProperty() 501 | keyPem.Set(pubString) 502 | pubk.SetW3IDSecurityV1PublicKeyPem(keyPem) 503 | 504 | pubkProp := streams.NewW3IDSecurityV1PublicKeyProperty() 505 | pubkProp.AppendW3IDSecurityV1PublicKey(pubk) 506 | person.SetW3IDSecurityV1PublicKey(pubkProp) 507 | 508 | db := tb.db 509 | if err = db.Create(c, person); err != nil { 510 | return 511 | } else if err = createEmptyOrderedCollection(c, db, inboxIRI); err != nil { 512 | return 513 | } else if err = createEmptyOrderedCollection(c, db, outboxIRI); err != nil { 514 | return 515 | } else if err = createEmptyCollection(c, db, followersIRI); err != nil { 516 | return 517 | } else if err = createEmptyCollection(c, db, followingIRI); err != nil { 518 | return 519 | } else if err = createEmptyCollection(c, db, likedIRI); err != nil { 520 | return 521 | } 522 | return 523 | } 524 | 525 | func createEmptyOrderedCollection(c context.Context, db *Database, idIRI *url.URL) error { 526 | col := streams.NewActivityStreamsOrderedCollection() 527 | id := streams.NewJSONLDIdProperty() 528 | id.Set(idIRI) 529 | col.SetJSONLDId(id) 530 | 531 | return db.Create(c, col) 532 | } 533 | 534 | func createEmptyCollection(c context.Context, db *Database, idIRI *url.URL) error { 535 | col := streams.NewActivityStreamsCollection() 536 | id := streams.NewJSONLDIdProperty() 537 | id.Set(idIRI) 538 | col.SetJSONLDId(id) 539 | 540 | return db.Create(c, col) 541 | } 542 | 543 | /* ServerHandler */ 544 | 545 | var _ ServerHandler = &TestServerClosure{} 546 | 547 | type TestServerClosure struct { 548 | *TestServer 549 | pathPrefix string 550 | } 551 | 552 | func (ts *TestServerClosure) Handle(i *Instruction) { 553 | ts.PathHandle(ts.pathPrefix, i) 554 | } 555 | 556 | func (ts *TestServerClosure) Update(pending, done []TestInfo, results []Result) { 557 | ts.PathUpdate(ts.pathPrefix, pending, done, results) 558 | } 559 | 560 | func (ts *TestServerClosure) Error(err error) { 561 | ts.PathError(ts.pathPrefix, err) 562 | } 563 | 564 | func (ts *TestServerClosure) MarkDone() { 565 | ts.PathMarkDone(ts.pathPrefix) 566 | } 567 | 568 | func (ts *TestServer) PathHandle(path string, i *Instruction) { 569 | ts.cacheMu.RLock() 570 | tb, ok := ts.cache[path] 571 | ts.cacheMu.RUnlock() 572 | if !ok { 573 | return 574 | } 575 | tb.stateMu.Lock() 576 | if i == nil { 577 | tb.state.I = Instruction{} 578 | } else { 579 | tb.state.I = *i 580 | } 581 | tb.stateMu.Unlock() 582 | } 583 | 584 | func (ts *TestServer) PathUpdate(path string, pending, done []TestInfo, results []Result) { 585 | ts.cacheMu.Lock() 586 | tb, ok := ts.cache[path] 587 | ts.cacheMu.Unlock() 588 | if !ok { 589 | return 590 | } 591 | tb.stateMu.Lock() 592 | tb.state.Pending = pending 593 | tb.state.Completed = done 594 | tb.state.Results = results 595 | tb.stateMu.Unlock() 596 | } 597 | 598 | func (ts *TestServer) PathError(path string, err error) { 599 | ts.cacheMu.RLock() 600 | tb, ok := ts.cache[path] 601 | ts.cacheMu.RUnlock() 602 | if !ok { 603 | return 604 | } 605 | tb.stateMu.Lock() 606 | tb.state.Err = err 607 | tb.stateMu.Unlock() 608 | } 609 | 610 | func (ts *TestServer) PathMarkDone(path string) { 611 | ts.cacheMu.RLock() 612 | tb, ok := ts.cache[path] 613 | ts.cacheMu.RUnlock() 614 | if !ok { 615 | return 616 | } 617 | tb.stateMu.Lock() 618 | tb.state.Done = true 619 | tb.stateMu.Unlock() 620 | } 621 | -------------------------------------------------------------------------------- /server/transport.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto" 7 | "crypto/x509" 8 | "encoding/json" 9 | "encoding/pem" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/go-fed/activity/pub" 19 | "github.com/go-fed/activity/streams" 20 | "github.com/go-fed/activity/streams/vocab" 21 | "github.com/go-fed/httpsig" 22 | ) 23 | 24 | type PlainTransport struct { 25 | client *http.Client 26 | appAgent string 27 | r *Recorder 28 | acceptProfile bool 29 | } 30 | 31 | func NewPlainTransportWithActivityJSON(r *Recorder) *PlainTransport { 32 | return &PlainTransport{ 33 | client: &http.Client{}, 34 | appAgent: "testserver (go-fed/testsuite)", 35 | r: r, 36 | acceptProfile: false, 37 | } 38 | } 39 | 40 | func NewPlainTransport(r *Recorder) *PlainTransport { 41 | return &PlainTransport{ 42 | client: &http.Client{}, 43 | appAgent: "testserver (go-fed/testsuite)", 44 | r: r, 45 | acceptProfile: true, 46 | } 47 | } 48 | 49 | func (p PlainTransport) DereferenceWithStatusCode(c context.Context, iri *url.URL) ([]byte, int, error) { 50 | req, err := http.NewRequest("GET", iri.String(), nil) 51 | if err != nil { 52 | return nil, 0, err 53 | } 54 | req = req.WithContext(c) 55 | if p.acceptProfile { 56 | req.Header.Add("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") 57 | } else { 58 | req.Header.Add("Accept", "application/activity+json") 59 | } 60 | req.Header.Add("Accept-Charset", "utf-8") 61 | req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") 62 | req.Header.Add("User-Agent", p.appAgent) 63 | p.r.Add("PlainTransport about to issue request", req) 64 | resp, err := p.client.Do(req) 65 | if err != nil { 66 | return nil, 0, err 67 | } 68 | defer resp.Body.Close() 69 | b, err := ioutil.ReadAll(resp.Body) 70 | return b, resp.StatusCode, err 71 | } 72 | 73 | func (p PlainTransport) Dereference(c context.Context, iri *url.URL) ([]byte, error) { 74 | req, err := http.NewRequest("GET", iri.String(), nil) 75 | if err != nil { 76 | return nil, err 77 | } 78 | req = req.WithContext(c) 79 | if p.acceptProfile { 80 | req.Header.Add("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") 81 | } else { 82 | req.Header.Add("Accept", "application/activity+json") 83 | } 84 | req.Header.Add("Accept-Charset", "utf-8") 85 | req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") 86 | req.Header.Add("User-Agent", p.appAgent) 87 | p.r.Add("PlainTransport about to issue request", req) 88 | resp, err := p.client.Do(req) 89 | if err != nil { 90 | return nil, err 91 | } 92 | defer resp.Body.Close() 93 | if resp.StatusCode != http.StatusOK { 94 | return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status) 95 | } 96 | return ioutil.ReadAll(resp.Body) 97 | } 98 | 99 | // isSuccess returns true if the HTTP status code is either OK, Created, or 100 | // Accepted. 101 | func isSuccess(code int) bool { 102 | return code == http.StatusOK || 103 | code == http.StatusCreated || 104 | code == http.StatusAccepted 105 | } 106 | 107 | func (p PlainTransport) Deliver(c context.Context, b []byte, to *url.URL) error { 108 | req, err := http.NewRequest("POST", to.String(), bytes.NewReader(b)) 109 | if err != nil { 110 | return err 111 | } 112 | req = req.WithContext(c) 113 | req.Header.Add("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") 114 | req.Header.Add("Accept-Charset", "utf-8") 115 | req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") 116 | req.Header.Add("User-Agent", fmt.Sprintf("%s %s", p.appAgent)) 117 | if err != nil { 118 | return err 119 | } 120 | resp, err := p.client.Do(req) 121 | if err != nil { 122 | return err 123 | } 124 | defer resp.Body.Close() 125 | if !isSuccess(resp.StatusCode) { 126 | return fmt.Errorf("POST request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status) 127 | } 128 | return nil 129 | } 130 | 131 | func (p PlainTransport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error { 132 | var wg sync.WaitGroup 133 | errCh := make(chan error, len(recipients)) 134 | for _, recipient := range recipients { 135 | wg.Add(1) 136 | go func(r *url.URL) { 137 | defer wg.Done() 138 | if err := p.Deliver(c, b, r); err != nil { 139 | errCh <- err 140 | } 141 | }(recipient) 142 | } 143 | wg.Wait() 144 | errs := make([]string, 0, len(recipients)) 145 | outer: 146 | for { 147 | select { 148 | case e := <-errCh: 149 | errs = append(errs, e.Error()) 150 | default: 151 | break outer 152 | } 153 | } 154 | if len(errs) > 0 { 155 | return fmt.Errorf("batch deliver had at least one failure: %s", strings.Join(errs, "; ")) 156 | } 157 | return nil 158 | } 159 | 160 | func HTTPSigTransport(c context.Context, a *ActorMapping) (t pub.Transport, err error) { 161 | prefs := []httpsig.Algorithm{httpsig.RSA_SHA256} 162 | digestPref := httpsig.DigestSha256 163 | getHeadersToSign := []string{httpsig.RequestTarget, "Date"} 164 | postHeadersToSign := []string{httpsig.RequestTarget, "Date", "Digest"} 165 | getSigner, _, err := httpsig.NewSigner(prefs, digestPref, getHeadersToSign, httpsig.Signature, 3600) 166 | if err != nil { 167 | return 168 | } 169 | postSigner, _, err := httpsig.NewSigner(prefs, digestPref, postHeadersToSign, httpsig.Signature, 3600) 170 | if err != nil { 171 | return 172 | } 173 | 174 | var pubKeyID string 175 | var privKey crypto.PrivateKey 176 | pubKeyID, _, privKey, err = a.GetKeyInfo(c) 177 | if err != nil { 178 | return 179 | } 180 | 181 | client := &http.Client{ 182 | Timeout: time.Second * 30, 183 | } 184 | t = pub.NewHttpSigTransport( 185 | client, 186 | "go-fed/testsuite", 187 | &Clock{}, 188 | getSigner, 189 | postSigner, 190 | pubKeyID, 191 | privKey) 192 | return 193 | } 194 | 195 | func verifyHttpSignatures(c context.Context, 196 | host string, 197 | client *http.Client, 198 | r *http.Request, 199 | a *ActorMapping) (remoteActor *url.URL, authenticated bool, err error) { 200 | // 1. Figure out what key we need to verify 201 | var v httpsig.Verifier 202 | v, err = httpsig.NewVerifier(r) 203 | if err != nil { 204 | return 205 | } 206 | kId := v.KeyId() 207 | var kIdIRI *url.URL 208 | kIdIRI, err = url.Parse(kId) 209 | if err != nil { 210 | return 211 | } 212 | // ASSUMPTION: Key is a fragment ID on the actor 213 | // No time to be robust here. 214 | remoteActor = &url.URL{ 215 | Scheme: kIdIRI.Scheme, 216 | Host: kIdIRI.Host, 217 | Path: kIdIRI.Path, 218 | } 219 | // 2. Get our user's credentials 220 | var pubKeyURLString string 221 | var privKey crypto.PrivateKey 222 | _, pubKeyURLString, privKey, err = a.GetKeyInfo(c) 223 | if err != nil { 224 | return 225 | } 226 | var pubKeyURL *url.URL 227 | pubKeyURL, err = url.Parse(pubKeyURLString) 228 | if err != nil { 229 | return 230 | } 231 | pubKeyId := pubKeyURL.String() 232 | // 3. Fetch the public key of the other actor using our credentials 233 | prefs := []httpsig.Algorithm{httpsig.RSA_SHA256} 234 | digestPref := httpsig.DigestSha256 235 | getHeadersToSign := []string{httpsig.RequestTarget, "Date"} 236 | postHeadersToSign := []string{httpsig.RequestTarget, "Date", "Digest"} 237 | getSigner, _, err := httpsig.NewSigner(prefs, digestPref, getHeadersToSign, httpsig.Signature, 3600) 238 | if err != nil { 239 | return 240 | } 241 | postSigner, _, err := httpsig.NewSigner(prefs, digestPref, postHeadersToSign, httpsig.Signature, 3600) 242 | if err != nil { 243 | return 244 | } 245 | tp := pub.NewHttpSigTransport( 246 | client, 247 | host, 248 | &Clock{}, 249 | getSigner, 250 | postSigner, 251 | pubKeyId, 252 | privKey) 253 | var b []byte 254 | b, err = tp.Dereference(c, kIdIRI) 255 | if err != nil { 256 | return 257 | } 258 | pKey, err := getPublicKeyFromResponse(c, b, kIdIRI) 259 | if err != nil { 260 | return 261 | } 262 | // 4. Verify the other actor's key 263 | algo := prefs[0] 264 | verErr := v.Verify(pKey, algo) 265 | authenticated = nil == verErr 266 | return 267 | } 268 | 269 | type publicKeyer interface { 270 | GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty 271 | } 272 | 273 | func getPublicKeyFromResponse(c context.Context, b []byte, keyId *url.URL) (p crypto.PublicKey, err error) { 274 | m := make(map[string]interface{}, 0) 275 | err = json.Unmarshal(b, &m) 276 | if err != nil { 277 | return 278 | } 279 | var t vocab.Type 280 | t, err = streams.ToType(c, m) 281 | if err != nil { 282 | return 283 | } 284 | pker, ok := t.(publicKeyer) 285 | if !ok { 286 | err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t) 287 | return 288 | } 289 | pkp := pker.GetW3IDSecurityV1PublicKey() 290 | if pkp == nil { 291 | err = fmt.Errorf("publicKey property is not provided") 292 | return 293 | } 294 | var pkpFound vocab.W3IDSecurityV1PublicKey 295 | for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() { 296 | if !pkpIter.IsW3IDSecurityV1PublicKey() { 297 | continue 298 | } 299 | pkValue := pkpIter.Get() 300 | var pkId *url.URL 301 | pkId, err = pub.GetId(pkValue) 302 | if err != nil { 303 | return 304 | } 305 | if pkId.String() != keyId.String() { 306 | continue 307 | } 308 | pkpFound = pkValue 309 | break 310 | } 311 | if pkpFound == nil { 312 | err = fmt.Errorf("cannot find publicKey with id: %s", keyId) 313 | return 314 | } 315 | pkPemProp := pkpFound.GetW3IDSecurityV1PublicKeyPem() 316 | if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { 317 | err = fmt.Errorf("publicKeyPem property is not provided or it is not embedded as a value") 318 | return 319 | } 320 | pubKeyPem := pkPemProp.Get() 321 | var block *pem.Block 322 | block, _ = pem.Decode([]byte(pubKeyPem)) 323 | if block == nil || block.Type != "PUBLIC KEY" { 324 | err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") 325 | return 326 | } 327 | p, err = x509.ParsePKIXPublicKey(block.Bytes) 328 | return 329 | } 330 | -------------------------------------------------------------------------------- /server/webfinger.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | type link struct { 9 | Rel string `json:"rel,omitempty"` 10 | Type string `json:"type,omitempty"` 11 | Href string `json:"href,omitempty"` 12 | Template string `json:"template,omitempty"` 13 | } 14 | 15 | type webfinger struct { 16 | Subject string `json:"subject,omitempty"` 17 | Aliases []string `json:"aliases,omitempty"` 18 | Links []link `json:"links,omitempty"` 19 | } 20 | 21 | func toWebfinger(host, username string, apIRI *url.URL) webfinger { 22 | return webfinger{ 23 | Subject: fmt.Sprintf("acct:%s@%s", username, host), 24 | Aliases: []string{ 25 | apIRI.String(), 26 | }, 27 | Links: []link{ 28 | { 29 | Rel: "self", 30 | Type: "application/activity+json", 31 | Href: apIRI.String(), 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | // RFC 6415 38 | func hostMeta(host string) string { 39 | return fmt.Sprintf(` 40 | 41 | 42 | `, host) 43 | } 44 | -------------------------------------------------------------------------------- /server/webserver.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | const ( 22 | kPathPrefixTests = "/tests/evaluate/" 23 | kPathHome = "/" 24 | kPathAbout = "/about" 25 | kPathTestNew = "/tests/new" 26 | kPathTestState = "/tests/status/" 27 | kPathInstructionResponse = "/tests/instructions/" 28 | kPathHostMeta = "/.well-known/host-meta" 29 | kPathWebfinger = "/.well-known/webfinger" 30 | kPathStatic = "/static/" 31 | kSiteTemplateName = "site" 32 | ) 33 | 34 | type WebServer struct { 35 | hostname string 36 | notifyName string 37 | notifyLink string 38 | logFile string 39 | logFileMu sync.Mutex 40 | home *template.Template 41 | about *template.Template 42 | newTest *template.Template 43 | testStatus *template.Template 44 | s *http.Server 45 | ts *TestServer 46 | } 47 | 48 | func NewWebServer(home *template.Template, 49 | about *template.Template, 50 | newTest *template.Template, 51 | testStatus *template.Template, 52 | s *http.Server, 53 | hostname string, 54 | testTimeout time.Duration, 55 | maxTests int, 56 | notifyName, notifyLink string, 57 | staticDir string, 58 | logFile string) *WebServer { 59 | ws := &WebServer{ 60 | hostname: hostname, 61 | notifyName: notifyName, 62 | notifyLink: notifyLink, 63 | logFile: logFile, 64 | home: home, 65 | about: about, 66 | newTest: newTest, 67 | testStatus: testStatus, 68 | s: s, 69 | ts: NewTestServer(hostname, kPathPrefixTests, testTimeout, maxTests), 70 | } 71 | mux := http.NewServeMux() 72 | mux.HandleFunc(kPathHome, ws.homepageHandler) 73 | mux.HandleFunc(kPathAbout, ws.aboutPageHandler) 74 | mux.HandleFunc(kPathPrefixTests, ws.testRequestHandler) 75 | mux.HandleFunc(kPathTestState, ws.testStatusHandler) 76 | mux.HandleFunc(kPathTestNew, ws.startTestHandler) 77 | mux.HandleFunc(kPathInstructionResponse, ws.instructionResponseHandler) 78 | mux.HandleFunc(kPathHostMeta, ws.hostMetaHandler) 79 | mux.HandleFunc(kPathWebfinger, ws.webfingerHandler) 80 | mux.Handle(kPathStatic, ws.staticHandler(staticDir)) 81 | s.Handler = mux 82 | s.RegisterOnShutdown(ws.shutdown) 83 | return ws 84 | } 85 | 86 | func (ws *WebServer) shutdown() { 87 | ws.ts.shutdown() 88 | } 89 | 90 | func (ws *WebServer) homepageHandler(w http.ResponseWriter, r *http.Request) { 91 | if r.Method == http.MethodGet { 92 | data := struct { 93 | NotifyName string 94 | NotifyLink string 95 | }{ 96 | NotifyName: ws.notifyName, 97 | NotifyLink: ws.notifyLink, 98 | } 99 | ws.home.ExecuteTemplate(w, kSiteTemplateName, data) 100 | } else { 101 | http.NotFound(w, r) 102 | } 103 | } 104 | 105 | func (ws *WebServer) aboutPageHandler(w http.ResponseWriter, r *http.Request) { 106 | if r.Method == http.MethodGet { 107 | data := struct { 108 | NotifyName string 109 | NotifyLink string 110 | }{ 111 | NotifyName: ws.notifyName, 112 | NotifyLink: ws.notifyLink, 113 | } 114 | ws.about.ExecuteTemplate(w, kSiteTemplateName, data) 115 | } else { 116 | http.NotFound(w, r) 117 | } 118 | } 119 | 120 | func (ws *WebServer) startTestHandler(w http.ResponseWriter, r *http.Request) { 121 | if r.Method == http.MethodGet { 122 | ws.newTest.ExecuteTemplate(w, kSiteTemplateName, nil) 123 | } else if r.Method == http.MethodPost { 124 | remoteActorIRI := r.PostFormValue("remote_actor_iri") 125 | testRemoteActorID, err := url.Parse(remoteActorIRI) 126 | if err != nil { 127 | http.Error(w, "Error parsing remote actor IRI: "+err.Error(), http.StatusBadRequest) 128 | return 129 | } 130 | c2sStr := r.PostFormValue("enable_social") 131 | s2sStr := r.PostFormValue("enable_federating") 132 | enableWebfingerStr := r.PostFormValue("enable_webfinger") 133 | maxDeliverRecurStr := r.PostFormValue("maximum_deliver_recursion") 134 | c2s := c2sStr == "true" 135 | s2s := s2sStr == "true" 136 | enableWebfinger := enableWebfingerStr == "true" 137 | var maxDeliverRecur int 138 | if s2s { 139 | maxDeliverRecur, err = strconv.Atoi(maxDeliverRecurStr) 140 | if err != nil { 141 | http.Error(w, "Error parsing maximum delivery recursion limit: "+err.Error(), http.StatusBadRequest) 142 | return 143 | } 144 | } 145 | testNumber := rand.Int() 146 | testNumberStr := fmt.Sprintf("%d", testNumber) 147 | pathPrefix := path.Join(kPathPrefixTests, testNumberStr) 148 | err = ws.logTestCreation( 149 | time.Now().Format(time.RFC3339), 150 | testNumberStr, 151 | r.RemoteAddr, 152 | r.Header.Get("X-Forwarded-For"), 153 | r.UserAgent(), 154 | testRemoteActorID.String(), 155 | c2sStr, 156 | s2sStr) 157 | if err != nil { 158 | http.Error(w, "Internal error preparing the test suite", http.StatusInternalServerError) 159 | return 160 | } 161 | err = ws.ts.StartTest(r.Context(), 162 | pathPrefix, 163 | c2s, 164 | s2s, 165 | enableWebfinger, 166 | maxDeliverRecur, 167 | testRemoteActorID) 168 | if err != nil { 169 | http.Error(w, "Error preparing test: "+err.Error(), http.StatusInternalServerError) 170 | return 171 | } 172 | redir := &url.URL{ 173 | Path: path.Join(kPathTestState, fmt.Sprintf("%d", testNumber)), 174 | } 175 | http.Redirect(w, r, redir.String(), http.StatusFound) 176 | } else { 177 | http.NotFound(w, r) 178 | } 179 | } 180 | 181 | func (ws *WebServer) testRequestHandler(w http.ResponseWriter, r *http.Request) { 182 | prefix, ok := PathToTestPathPrefix(r.URL) 183 | if !ok { 184 | http.NotFound(w, r) 185 | return 186 | } 187 | c := context.WithValue(r.Context(), kContextKeyTestPrefix, prefix) 188 | ws.ts.HandleWeb(c, w, r) 189 | } 190 | 191 | func (ws *WebServer) testStatusHandler(w http.ResponseWriter, r *http.Request) { 192 | if r.Method == http.MethodGet { 193 | pathPrefix, ok := StatePathToTestPathPrefix(r.URL) 194 | if !ok { 195 | http.NotFound(w, r) 196 | return 197 | } 198 | state, ok := ws.ts.TestState(pathPrefix) 199 | if !ok { 200 | http.NotFound(w, r) 201 | return 202 | } 203 | err := ws.testStatus.ExecuteTemplate(w, kSiteTemplateName, state) 204 | if err != nil { 205 | log.Println(err) 206 | } 207 | } else { 208 | http.NotFound(w, r) 209 | } 210 | } 211 | 212 | func (ws *WebServer) instructionResponseHandler(w http.ResponseWriter, r *http.Request) { 213 | if r.Method == http.MethodPost { 214 | pathPrefix, ok := InstructionResponsePathToTestPathPrefix(r.URL) 215 | if !ok { 216 | http.NotFound(w, r) 217 | return 218 | } 219 | statePath, ok := InstructionResponsePathToTestState(r.URL) 220 | if !ok { 221 | http.NotFound(w, r) 222 | return 223 | } 224 | err := r.ParseForm() 225 | if err != nil { 226 | http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest) 227 | } 228 | ws.ts.HandleInstructionResponse(pathPrefix, r.Form) 229 | redir := &url.URL{ 230 | Path: statePath, 231 | } 232 | http.Redirect(w, r, redir.String(), http.StatusFound) 233 | } else { 234 | http.NotFound(w, r) 235 | } 236 | } 237 | 238 | func (ws *WebServer) hostMetaHandler(w http.ResponseWriter, r *http.Request) { 239 | w.Header().Set("Content-Type", "application/xrd+xml") 240 | hm := hostMeta(ws.hostname) 241 | io.WriteString(w, hm) 242 | } 243 | 244 | const ( 245 | // This is an unreserved character of RFC 3986 Section 2.3 246 | kWebfingerTestDelim = "." 247 | ) 248 | 249 | func (ws *WebServer) webfingerHandler(w http.ResponseWriter, r *http.Request) { 250 | q := r.URL.Query() 251 | userHost := strings.Split( 252 | strings.TrimPrefix(q.Get("resource"), "acct:"), 253 | "@") 254 | if len(userHost) != 2 { 255 | http.Error(w, "Error parsing query: "+q.Get("resource"), http.StatusBadRequest) 256 | return 257 | } 258 | userTest := strings.Split(userHost[0], kWebfingerTestDelim) 259 | if len(userTest) != 2 { 260 | http.Error(w, "Error parsing test and user: "+userHost[0], http.StatusBadRequest) 261 | return 262 | } 263 | user := userTest[0] 264 | pathPrefix := testPathPrefixFromId(userTest[1]) 265 | username, apIRI, err := ws.ts.HandleWebfinger(pathPrefix, user) 266 | if err != nil { 267 | http.Error(w, err.Error(), http.StatusBadRequest) 268 | return 269 | } 270 | wf := toWebfinger(ws.hostname, username, apIRI) 271 | b, err := json.Marshal(wf) 272 | if err != nil { 273 | http.Error(w, err.Error(), http.StatusInternalServerError) 274 | return 275 | } 276 | w.Header().Set("Content-Type", "application/jrd+json") 277 | w.Write(b) 278 | } 279 | 280 | func (ws *WebServer) staticHandler(dir string) http.Handler { 281 | fs := http.FileServer(http.Dir(dir)) 282 | return http.StripPrefix(kPathStatic, fs) 283 | } 284 | 285 | func (ws *WebServer) logTestCreation(s ...string) error { 286 | ws.logFileMu.Lock() 287 | defer ws.logFileMu.Unlock() 288 | f, err := os.OpenFile(ws.logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 289 | if err != nil { 290 | return err 291 | } 292 | defer f.Close() 293 | if _, err = f.WriteString(strings.Join(s, ",") + "\n"); err != nil { 294 | return err 295 | } 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /static/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | height: 100%; 9 | font-family: 'RawlineRegular', 'Lucida Grande', sans-serif; 10 | font-weight: normal; 11 | font-style: normal; 12 | background: white; 13 | color: black; 14 | } 15 | 16 | #container { 17 | min-height: 100%; 18 | position: relative; 19 | } 20 | 21 | #footerpusher { 22 | padding-bottom: 140px; 23 | } 24 | 25 | .sitename { 26 | font-size: 2.7em; 27 | margin: auto 0; 28 | } 29 | 30 | .sitename > a { 31 | text-decoration: none; 32 | } 33 | 34 | #content { 35 | margin: 2em auto 2em auto; 36 | width: 72%; 37 | } 38 | 39 | footer { 40 | text-align: center; 41 | position: absolute; 42 | bottom: 0; 43 | height: 140px; 44 | width: 100%; 45 | background: #000000; 46 | color: #cfcfcf; 47 | } 48 | 49 | footer > div { 50 | margin-top: 1em; 51 | } 52 | 53 | a, a:visited { 54 | color: #f1007e; 55 | } 56 | 57 | nav { 58 | display: flex; 59 | padding: 0.2em 5em; 60 | background: #000000; 61 | } 62 | 63 | nav > ul { 64 | margin: auto 0; 65 | font-size: 2em; 66 | padding-inline-start: 1em; 67 | } 68 | 69 | nav > ul > li { 70 | display: inline-block; 71 | margin: 0 0.4em; 72 | } 73 | 74 | nav > ul > li > a { 75 | text-decoration: none; 76 | padding: 0 0.2em; 77 | } 78 | 79 | nav > ul > li > a:focus, nav > ul > li > a:hover, nav > ul > li > a:active { 80 | text-decoration: underline; 81 | } 82 | 83 | .notice { 84 | text-align: center; 85 | border-color: #f1007e; 86 | border-style: solid; 87 | border-width: 1px; 88 | border-radius: 3px; 89 | } 90 | 91 | button, input { 92 | font-family: inherit; 93 | font-size: 100%; 94 | appearance: none; 95 | -webkit-appearance: none; 96 | } 97 | 98 | input[type="text"] { 99 | border-top-style: hidden; 100 | border-left-style: hidden; 101 | border-right-style: hidden; 102 | border-bottom-style: solid; 103 | border-color: #f1007e; 104 | border-width: 1px; 105 | background: #cfcfcf; 106 | width: 500px; 107 | } 108 | 109 | input[type="number"] { 110 | border-top-style: hidden; 111 | border-left-style: hidden; 112 | border-right-style: hidden; 113 | border-bottom-style: solid; 114 | border-color: #f1007e; 115 | border-width: 1px; 116 | background: #cfcfcf; 117 | width: 50px; 118 | } 119 | 120 | input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { 121 | opacity: 1; 122 | } 123 | 124 | input[type="checkbox"] { 125 | position: relative; 126 | width: 1em; 127 | height: 1em; 128 | border: 1px solid #f1007e; 129 | vertical-align: -0.25em; 130 | background: #cfcfcf; 131 | } 132 | 133 | input[type="checkbox"]::before { 134 | position: absolute; 135 | content: "✕"; 136 | color: #f1007e; 137 | visibility: hidden; 138 | } 139 | 140 | input[type="checkbox"]:checked::before { 141 | visibility: visible; 142 | } 143 | 144 | input[type="checkbox"]:disabled { 145 | background: black; 146 | } 147 | 148 | input[type="submit"], button { 149 | border: 1px solid #f1007e; 150 | border-radius: 2px; 151 | background: #cfcfcf; 152 | } 153 | 154 | :focus { 155 | outline: 3px solid #f1007e; 156 | } 157 | 158 | .greystatus { 159 | background: #8b8b8b; 160 | border: 2px solid #101010; 161 | padding: 0.2em 1em; 162 | } 163 | 164 | .redstatus { 165 | background: #ff8b8b; 166 | border: 2px solid #ef1010; 167 | padding: 0.2em 1em; 168 | } 169 | 170 | .greenstatus { 171 | background: #8bff8b; 172 | border: 2px solid #10ef10; 173 | padding: 0.2em 1em; 174 | } 175 | 176 | .yellowstatus { 177 | background: #ffff8b; 178 | border: 2px solid #efef10; 179 | padding: 0.2em 1em; 180 | } 181 | 182 | .form { 183 | padding: 0.2em 1em; 184 | border: 1px solid #f1007e; 185 | font-family: 'HackRegular', monospace; 186 | } 187 | 188 | .mono { 189 | font-family: 'HackRegular', monospace; 190 | } 191 | 192 | table { 193 | table-layout: fixed; 194 | border-collapse: collapse; 195 | border: 1px solid #f1007e; 196 | font-family: 'HackRegular', monospace; 197 | } 198 | 199 | th, td { 200 | padding: 0.2em 0.4em; 201 | border: 1px dashed #f1007e; 202 | } 203 | 204 | td { 205 | font-size: 0.9em; 206 | } 207 | 208 | .greenrow { 209 | background: #8bff8b; 210 | } 211 | 212 | .redrow { 213 | background: #ff8b8b; 214 | } 215 | 216 | .greyrow { 217 | background: #8b8b8b; 218 | } 219 | 220 | details { 221 | border: 1px solid #f1007e; 222 | margin: 1em 0; 223 | padding: 0.2em 1em; 224 | } 225 | 226 | .logentries { 227 | font-family: 'HackRegular', monospace; 228 | } 229 | 230 | .logentry { 231 | padding: 0 1em; 232 | border: 1px dashed #f1007e; 233 | margin: 0.2em 0; 234 | } 235 | 236 | .sublogentry { 237 | margin-left: 3em; 238 | } 239 | -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}ActivityPub Dev -- About{{end}} 2 | {{define "body"}}

About

3 |

There are 4 | a 5 | lot 6 | of 7 | duplicate 8 | issues 9 | about the official test suite being down. There has been an effort started 10 | by its author 11 | on the SocialHub about 12 | trying to resurrect the test suite, but unfortunately not much progress has 13 | been made, and folks have tried! As an aside, it is worth joining the 14 | SocialHub. Back on topic: this test suite attempts to supplement 15 | the original should it make its way back online.

16 |

This suite is built using the 17 | go-fed library in order to create instanced 18 | and short-lived ActivityPub actors for test runs. The tests are roughly 19 | equivalent ports from the official test suite. However, the official test 20 | suite relied on a lot of questionnaire-style prompts for the S2S federation 21 | tests. While this unofficial test suite cannot eliminate all of them, a 22 | solid number have been replaced with machine-assisted tests.

23 |

This is a community resource, please use it kindly.

24 |

Should you need to get in contact with the operator, please reach out 25 | to 26 | {{.NotifyName}}.

27 | {{end}} 28 | -------------------------------------------------------------------------------- /templates/common.tmpl: -------------------------------------------------------------------------------- 1 | {{define "common_top"}} 2 | 3 | 4 | 5 | 6 | {{template "title"}} 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 21 |
22 |
23 |
24 |

This unofficial testsuite is in alpha. Please file a 25 | bug report 26 | if you encounter an issue.

27 |
28 | {{end}} 29 | {{define "common_bottom"}} 30 |
31 | 45 |
46 | 47 | {{end}} 48 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}ActivityPub Dev -- Unofficial Test Suite{{end}} 2 | {{define "body"}}

Unofficial ActivityPub Testsuite

3 |

This server allows you to run a machine-assisted test suite against 4 | an ActivityPub application. It is a port of the old official test suite. 5 | While the old test suite was comprehensive for C2S and S2S, it was mostly 6 | a questionnaire for the S2S portion. This test suite aims to minimize the 7 | "on your honor" questions, though it cannot eliminate all of them.

8 |

When you run a test suite, please take notice of the following 9 | considerations:

10 |
    11 |
  • You run tests only against servers you own
  • 12 |
  • You do not use the testsuite for spam & abuse
  • 13 |
  • You do not abuse the testsuite server
  • 14 |
15 |

As such, usage of these testsuites is monitored.

16 |

If the community cannot be respectful in its usage of this testsuite 17 | then it will no 18 | longer be hosted as a freely available resource. To notify of such 19 | incidents, please reach out to 20 | {{.NotifyName}}.

21 |

Let's start a new test.

22 | {{end}} 23 | -------------------------------------------------------------------------------- /templates/new_test.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}ActivityPub Dev -- Unofficial Test Suite -- New Test Suite{{end}} 2 | {{define "body"}}

Run A New Test Suite

3 |

To run a new testsuite against your test server, please submit 4 | the following form. Once you submit the form, you will be taken to 5 | the test status page. It does not use javascript, so you will need 6 | to periodically refresh the page until the tests are all terminated, 7 | or until you are prompted with instructions. When you are presented 8 | with instructions, the test suite will pause its execution of further 9 | tests.

10 |

Please note these additional considerations when running a test:

11 |
    12 |
  • There are currently no C2S tests.
  • 13 |
  • The test suite will only live for 15 minutes before being 14 | garbage collected.
  • 15 |
  • There is a known problem with the "Done" button in the 16 | one instruction that prompts for it.
  • 17 |
  • Early on, a test will prompt you to "Block" a test actor, 18 | but that actor will be re-used in later tests. Oops!
  • 19 |
  • One or two tests use a 5-second sleep. If you've completed 20 | a behavior and see no progress, please be patient!
  • 21 |
22 |

To start, you will need a single test Actor ID hosted on your 23 | server for this test suite to run tests against.

24 |
25 |
26 |

27 |

28 |

29 |

30 |

31 |

32 |

General Test Options

33 |

34 |

35 |

S2S-Only Test Options

36 |

37 |

38 |

Ready?

39 |

40 |
41 |
42 | {{end}} 43 | -------------------------------------------------------------------------------- /templates/site.tmpl: -------------------------------------------------------------------------------- 1 | {{define "site"}} 2 | {{template "common_top"}} 3 | {{template "body" .}} 4 | {{template "common_bottom"}} 5 | {{end}} 6 | -------------------------------------------------------------------------------- /templates/test_status.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}ActivityPub Dev -- Test Suite -- Test Status{{end}} 2 | {{define "body"}}

Test Status: {{.ID}}

3 | {{if .Done}} 4 | Status: Done 5 | {{else}} 6 | Status: Running 7 | {{end}} 8 | {{if .Err}} 9 | Error: {{.Err.Error}} 10 | {{else}} 11 | No Errors 12 | {{end}} 13 | {{if lt 0 (len .I.TestName)}} 14 | Requires Instructions 15 | {{else}} 16 | No Instructions 17 | {{end}} 18 | Passed: {{.NPass}} 19 | Inconclusive: {{.NInconclusive}} 20 | Failed: {{.NFail}} 21 | 31 | 32 |

Instructions

33 | {{if lt 0 (len .I.TestName)}} 34 |

To continue testing, please follow these instructions:

35 |
36 |

{{.I.Instructions}}

37 |
38 | {{$has_only_labelonly := true}} 39 | {{range $i, $ir := .I.Resp}} 40 |

41 | {{if eq $ir.Type "text_box"}} 42 |

43 | {{$has_only_labelonly = false}} 44 | {{else if eq $ir.Type "checkbox"}} 45 |

46 | {{$has_only_labelonly = false}} 47 | {{else if eq $ir.Type "label_only"}} 48 |

49 | {{else if eq $ir.Type "done_button"}} 50 |

51 | {{else if eq $ir.Type "number"}} 52 |

53 | {{$has_only_labelonly = false}} 54 | {{else if eq $ir.Type "yes_no"}} 55 | 56 | 57 | 58 | 59 | {{$has_only_labelonly = false}} 60 | {{else}} 61 | Error: unhandled input type {{$ir.Type}}

62 | {{end}} 63 | {{end}} 64 | {{if .I.Skippable}} 65 |

66 | {{end}} 67 | {{if not $has_only_labelonly}} 68 |

69 | {{end}} 70 |
71 |
72 | {{else}} 73 |

There are no instructions.

74 | {{end}} 75 | 76 |

Tests

77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
StatusNumber
Pending{{len .Pending}}
Completed{{len .Completed}}
93 | 94 |

Pending

95 | {{if eq 0 (len .Pending)}} 96 |

No pending tests

97 | {{else}} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {{range $i, $v := .Pending}} 108 | 109 | 110 | 111 | 112 | 113 | {{end}} 114 | 115 |
Test NameDescriptionKind
{{$v.TestName}}{{$v.Description}}{{$v.SpecKind}}
116 | {{end}} 117 | 118 |

Results

119 | {{if eq 0 (len .Results)}} 120 |

No test results

121 | {{else}} 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {{range $i, $v := .Results}} 133 | 134 | 135 | 136 | 137 | 138 | 139 | {{end}} 140 | 141 |
Test NameKindStateLog
{{$v.TestName}}{{$v.SpecKind}}{{$v.State}}Link
142 | {{end}} 143 | 144 |

Logs

145 | {{if eq 0 (len .Results)}} 146 |

No test logs

147 | {{else}} 148 | {{range $i, $v := .Results}} 149 |
150 | {{$v.TestName}} 151 |
152 | {{range $i, $e := $v.Records.Entries}} 153 |

{{$e.T}}: {{$e.Msg}}

154 | {{if gt (len $e.M) 0}} 155 |

{{$e.MString}}

156 | {{end}} 157 |
158 | {{end}} 159 |
160 |
161 | {{end}} 162 | {{end}} 163 | {{end}} 164 | --------------------------------------------------------------------------------