├── 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}}.
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}}.
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.