Hi Massimo,I tested "::rivet::var_post all" by inserting it into my client session code:
# DEBUG: What does var_post return? if {[info exists env(REQUEST_METHOD)] && [string match "POST" $env(REQUEST_METHOD)]} { ::rivet::apache_log_error debug "clientSession: POST, ::rivet::var_post=[::rivet::var_post all]" }... but when a Fossil POST request arrives, this function returns a blank string. Probably because the POST request uses no variables. It's main purpose appears to be to pass Fossil the 100 or so bytes in the body of the POST request.
When I ask Fossil to do a http clone and observe it with Wireshark at the server I see the following:
Frame 261: 164 bytes on wire (1312 bits), 164 bytes captured (1312 bits) on interface 0 Ethernet II, Src: d8:5e:d3:0e:20:aa (d8:5e:d3:0e:20:aa), Dst: AsrockIn_e6:6b:35 (bc:5f:f4:e6:6b:35)Internet Protocol Version 4, Src: 192.168.1.12, Dst: 192.168.1.1Transmission Control Protocol, Src Port: 33820, Dst Port: 80, Seq: 180, Ack: 1, Len: 98[2 Reassembled TCP Segments (277 bytes): #259(179), #261(98)] [Frame: 259, payload: 0-178 (179 bytes)] [Frame: 261, payload: 179-276 (98 bytes)] [Segment count: 2] [Reassembled TCP length: 277][Reassembled TCP Data: 504f5354202f666f73732f67697432666f7373696c2e6367...]Hypertext Transfer Protocol POST /foss/git2fossil.cgi HTTP/1.0\r\n Host: www.server2.svpts\r\n User-Agent: Fossil/2.20 (2022-11-16 18:46:32 [210e89a059])\r\n Content-Type: application/x-fossil\r\n Content-Length: 98\r\n \r\n [Full request URI: http://www.server2.svpts/foss/git2fossil.cgi] [HTTP request 1/1] [Response in frame: 273] File Data: 98 bytes Media Type Media type: application/x-fossil (98 bytes) 0000 00 00 00 61 78 da 05 c1 41 0a 80 20 10 05 d0 bd ...ax... A.. .... 0010 a7 18 68 1d cc fc 19 35 d7 69 f7 88 90 08 ca a2 ..h....5 .i...... 0020 a2 f3 f7 de 75 cf eb 31 d3 b2 6f b5 bd fd 57 ef ....u..1 ..o...W. 0030 67 3b 1b 01 cc 4c 60 40 44 02 c9 60 41 e1 96 fd g;...L`@ D..`A... 0040 6c 95 94 c4 75 c4 d1 4f 01 69 9c 3c 52 56 68 41 l...u..O .i.<RVhA 0050 31 d3 1c 63 32 b3 11 52 82 64 5f 72 74 3f 31 1c 1..c2..R .d_rt?1. 0060 18 6d .m
The 98 bytes in the body of the request after the last "\r\n" is the part I'm trying to get hold of in my website code. With Rivet patched (attached to this email) the ::rivet::raw_post returns the 98 bytes as a Tcl byte array, and I'm able to write that directly through the pipe to Fossil's, and I had the first successful "fossil clone http..." via Rivet and the website last night. I was very happy!
For reference I've also attached my execcgi.tcl module. Much of it is notes and documentation I've gleaned and used as a guide. I've tested it with a few CGI programs not just Fossil. It's really a work in progress but also seems to doing the job now.
Thanks for your help :) Kind regards, Scott On 11/4/23 02:31, Massimo Manghi wrote:
Hi ScottThank you for the extensive explanation of your problem. I'm certainly interested in reviewing your patchWhy using the usual command ::rivet::var_post (see https://tcl.apache.org/rivet/manual3.2/var.html) to retrieve the posted data didn't work for you?-- Massimo On 09/04/23 01:55, Scott Pitcher wrote:Hi,I've been writing a system for our website using Rivet as the foundation. I'd written a proc for running CGI programs from the client request, or, passing the request over. This works well but I'd recently started to incorporate some fossil archives in the website. The fossil CGI method worked except when I tried to clone the repo from a shell. Fossil would report an error code and clone would fail.I examined the website logs and it Fossil makes a POST request to the CGI program. With wireshark I could see around 100 or so binary bytes in the body of the POST request, below the headers:0000 50 4f 53 54 20 2f 66 6f 73 73 2f 75 6e 69 6d 61 POST /foss/unima 0010 6b 65 73 63 72 69 70 74 73 2e 63 67 69 2f 69 6e kescripts.cgi/in 0020 64 65 78 20 48 54 54 50 2f 31 2e 30 0d 0a 48 6f dex HTTP/1.0..Ho 0030 73 74 3a 20 77 77 77 2e 73 65 72 76 65 72 32 2e st: www.server2. 0040 73 76 70 74 73 0d 0a 55 73 65 72 2d 41 67 65 6e svpts..User-Agen 0050 74 3a 20 46 6f 73 73 69 6c 2f 32 2e 32 30 20 28 t: Fossil/2.20 ( 0060 32 30 32 32 2d 31 31 2d 31 36 20 31 38 3a 34 36 2022-11-16 18:46 0070 3a 33 32 20 5b 32 31 30 65 38 39 61 30 35 39 5d :32 [210e89a059] 0080 29 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a )..Content-Type: 0090 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 2d 66 application/x-f 00a0 6f 73 73 69 6c 0d 0a 43 6f 6e 74 65 6e 74 2d 4c ossil..Content-L 00b0 65 6e 67 74 68 3a 20 31 30 30 0d 0a 0d 0a 00 00 ength: 100...... 00c0 00 61 78 da 05 c1 41 0a 80 20 10 00 c0 bb af 58 .axÚ.ÁA.. ..À»¯X 00d0 e8 1c ec 6e ba e9 d1 4c ff 21 21 11 94 86 45 ef è.ìnºéÑLÿ!!...Eï 00e0 6f e6 ee 79 bf 32 6c e7 51 ea 3b 7e a5 3f 47 ab oæîy¿2lçQê;~¥?G« 00f0 c0 8c 88 c0 c8 4c 44 02 64 b5 4c ac b6 b3 d5 02 À..ÀÈLD.dµL¬¶³Õ. 0100 13 90 1a 20 a1 5d 30 08 e9 e0 8c 8e 5e 4b 8c 22 ... ¡]0.éà..^K." 0110 c6 27 f4 f3 6c 53 10 71 d6 38 b7 92 fa 01 36 4f Æ'ôólS.qÖ8·.ú.6O 0120 18 9aI was able to retrieve the headers using ::rivet::apache_table array_get headers_in. I wasn't able to retrieve the body of the POST request. I tried ::rivet::raw_post but it returns an empty string each time.I checked out the Rivet source and delved into src/mod_rivet_ng and the Rivet_RawPost() function, and traced down to apache_request.h and ApacheRequest_get_raw_post().The problem is that Rivet_RawPost() uses Tcl_NewStringObj() to pass the body back to Tcl, and given that the first 3 bytes are NUL, we end with an empty string. I changed it to Tcl_NewByteArrayObj() and added a length parameter which was passed by address all the way down to ApacheRequest_get_raw_post():data = TclWeb_GetRawPost(private->req, &length); if (!data) { data = ""; } retval = Tcl_NewByteArrayObj((const unsigned char *)data, length); Tcl_SetObjResult(interp, retval);... and added a raw_length field to the ApacheRequest structure:const char* temp_dir; char* raw_post; /* Raw post data. */ int raw_length; request_rec* r; int nargs; } ApacheRequest;The ApacheRequest_parse_urlencoded() function will fill this field in just after filling in the raw_post pointer, and util_read needed a length pointer parameter as well to get that working:int ApacheRequest_parse_urlencoded(ApacheRequest *req) { ..... if ((rc = util_read(req, &data, &length)) != OK) { return rc; } if (data) { req->raw_post = (char*) data; /* Give people a way of getting at the raw data. */ req->raw_length = length; split_to_parms(req, data); } } return OK; }I went to the effort because it's a simple change and I think it completes raw_post , if that function can return the complete body.That's just my opinion. It's important to me because I have a website structure that can handle content, both HTTP and HTTPS at the same time, as well as user base with login and differentiation between local and remote access. It's been developed so we can include private content for our business only and host files for customers both public (e.g. documentation) and private (firmware and software downloads).I really wanted to get fossil working under it as it streamlines that part of the development for us. I was going to give up and host the fossil archives separately but I persisted and came up with this. I still haven't had time to pass the POST request into fossil but I'll do so when I have time this weekend, but I do expect it to work, one way or another.Anyway. That's my situation and if it's at all any use to anyone, I'd be more than happy to give you a patch. It's a really a minor set of changes but they give raw_post a bit more utility.Further: I'd really like Rivet to have a kind of "run_cgi" method, but I've coded one in tcl at the moment and that's good for testing. At some stage I'll write a Rivet command for doing the same which might make environment setup and processing of the CGI output, headers etc, a bit more efficient.Kind regards, Scott Pitcher--------------------------------------------------------------------- To unsubscribe, e-mail: rivet-dev-unsubscr...@tcl.apache.org For additional commands, e-mail: rivet-dev-h...@tcl.apache.org
-- +--------------------------------------------------------------------------------+ | ... will make the wilderness a pool of water, the dry land springs of water ...| +--------------------------------------------------------------------------------+ | Scott pitchersco...@svptechnicalservices.com.au | | +61-3-9008-5695 (B) +61-479-042-834 (M) | | SVP Technical Services ABN 79 220 496 603 | |PGP:https://gnupg.org/ | |KEY:https://pgp.mit.edu/pks/lookup?op=vindex&search=0xEDBE43639EFD884D | +--------------------------------------------------------------------------------+ DISCLAIMER: The content of this email is confidential and strictly intended only for the recipient(s) specified in the message. If you have received this message by mistake it is strictly forbidden to keep this email in any form, electronic or hard copy, or to share any part of it with any one else, without the prior, written consent of the author. If you have received this message by mistake, please let me know by replying to this email, and then destroy it and any copies. SECURITY: I prefer to send and receive confidential messages protected by PGP public key encryption. If you are willing to or are interested in protecting our email communication with PGP encryption, please refer tohttps://gnupg.org/.
diff --git a/src/mod_rivet_ng/TclWebapache.c b/src/mod_rivet_ng/TclWebapache.c index b9191f1..1b9ed59 100644 --- a/src/mod_rivet_ng/TclWebapache.c +++ b/src/mod_rivet_ng/TclWebapache.c @@ -1079,7 +1079,7 @@ TclWeb_GetVirtualFile(TclWebRequest *req, char *virtualname) */ char * -TclWeb_GetRawPost ( TclWebRequest *req ) +TclWeb_GetRawPost ( TclWebRequest *req, int *len ) { - return ApacheRequest_get_raw_post(req->apachereq); + return ApacheRequest_get_raw_post(req->apachereq, len); } diff --git a/src/mod_rivet_ng/rivetCore.c b/src/mod_rivet_ng/rivetCore.c index 09a9d86..ffa4381 100644 --- a/src/mod_rivet_ng/rivetCore.c +++ b/src/mod_rivet_ng/rivetCore.c @@ -63,7 +63,7 @@ #define COOKIES_ARRAY_NAME "cookies" extern module rivet_module; -extern char* TclWeb_GetRawPost (TclWebRequest *req); +extern char* TclWeb_GetRawPost (TclWebRequest *req, int *len); extern mod_rivet_globals* module_globals; extern apr_threadkey_t* rivet_thread_key; @@ -1274,18 +1274,19 @@ TCL_CMD_HEADER( Rivet_Upload ) TCL_CMD_HEADER ( Rivet_RawPost ) { char* data; + int length; Tcl_Obj* retval; rivet_thread_private* private; THREAD_PRIVATE_DATA(private) CHECK_REQUEST_REC(private,"::rivet::raw_post") - data = TclWeb_GetRawPost(private->req); + data = TclWeb_GetRawPost(private->req, &length); if (!data) { data = ""; } - retval = Tcl_NewStringObj(data, -1); + retval = Tcl_NewByteArrayObj((const unsigned char *)data, length); Tcl_SetObjResult(interp, retval); return TCL_OK; } diff --git a/src/mod_rivet_ng/rivet_types.h b/src/mod_rivet_ng/rivet_types.h index d3f30ca..8aebea0 100644 --- a/src/mod_rivet_ng/rivet_types.h +++ b/src/mod_rivet_ng/rivet_types.h @@ -75,6 +75,7 @@ typedef struct _ApacheRequest { void* hook_data; const char* temp_dir; char* raw_post; /* Raw post data. */ + int raw_length; request_rec* r; int nargs; } ApacheRequest; diff --git a/src/request/apache_request.c b/src/request/apache_request.c index 0db8ec5..1cbda0d 100644 --- a/src/request/apache_request.c +++ b/src/request/apache_request.c @@ -37,7 +37,7 @@ static void req_plustospace(char *str) } static int -util_read(ApacheRequest *req, const char **rbuf) +util_read(ApacheRequest *req, const char **rbuf, int *rlen) { request_rec *r = req->r; int rc = OK; @@ -61,6 +61,7 @@ util_read(ApacheRequest *req, const char **rbuf) } *rbuf = apr_pcalloc(r->pool, length + 1); + *rlen = length; while ((len_read = ap_get_client_block(r, buff, sizeof(buff))) > 0) { @@ -215,6 +216,7 @@ ApacheRequest *ApacheRequest_new(apr_pool_t *pool) req->hook_data = NULL; req->temp_dir = NULL; req->raw_post = NULL; + req->raw_length = 0; req->parsed = 0; req->r = NULL; req->nargs = 0; @@ -234,6 +236,7 @@ ApacheRequest *ApacheRequest_init(ApacheRequest* req, request_rec *r) req->hook_data = NULL; req->temp_dir = NULL; req->raw_post = NULL; + req->raw_length = 0; req->parsed = 0; req->r = r; req->nargs = 0; @@ -432,6 +435,7 @@ int ApacheRequest_parse_urlencoded(ApacheRequest *req) if (r->method_number == M_POST || r->method_number == M_PUT || r->method_number == M_DELETE) { const char *data = NULL; + int length = 0; /* const char *type; @@ -443,12 +447,13 @@ int ApacheRequest_parse_urlencoded(ApacheRequest *req) } */ - if ((rc = util_read(req, &data)) != OK) { + if ((rc = util_read(req, &data, &length)) != OK) { return rc; } if (data) { req->raw_post = (char*) data; /* Give people a way of getting at the raw data. */ + req->raw_length = length; split_to_parms(req, data); } } @@ -738,3 +743,9 @@ char *ApacheRequest_expires(ApacheRequest *req, char *time_str) return ApacheUtil_expires(req->r->pool, time_str, EXPIRES_HTTP); } +char *ApacheRequest_get_raw_post(ApacheRequest *req, int *len) +{ + if (len) + *len = req->raw_length; + return req->raw_post; +} diff --git a/src/request/apache_request.h b/src/request/apache_request.h index 0e05b95..f9e0c5e 100644 --- a/src/request/apache_request.h +++ b/src/request/apache_request.h @@ -121,7 +121,7 @@ ApacheUpload *ApacheUpload_find(ApacheUpload *upload, char *name); #define ApacheRequest_set_post_max(req, max) ((req)->post_max = (max)) #define ApacheRequest_set_temp_dir(req, dir) ((req)->temp_dir = (dir)) -#define ApacheRequest_get_raw_post(req) ((req)->raw_post) +char *ApacheRequest_get_raw_post(ApacheRequest *req, int *len); char *ApacheUtil_expires(apr_pool_t *p, char *time_str, int type); #define EXPIRES_HTTP 1
#!/usr/bin/tclsh # # execcgi.tcl # # Copyright (C) 2021-2023 - Scott V Pitcher # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You can find a copy of the GNU Lesser General Public License at: # <https://www.gnu.org/licenses/>. # # ---------------------------------------------------------------------------------------------------- # # Handle CGI execution for content with the -cgi option. # namespace eval ::Website { # A quick escape for the environment strings. # We can't use ::rivet::escape_shell_command because it inserts extra back slashes into the strings. # Escapes: # ; Semi colons. # " " Spaces # proc CgiExecEscape {str} { return [string map {{;} {\;} { } {\ }} $str] } # Map a header string with mixed case and dashes to an environment string. # Escapes: # none. # Maps: # lower case to upper. # '-' to '_'. # proc CgiHdrStrToEnv {str} { return [string toupper [string map {{-} {_}} $str]] } # # Execute a CGI file. # TODO: We'll have to use /usr/bin/env to set the CGI environment beforehand. # # From fossil's cgi.c: # -------------------- # # ** CGI Parameter quick reference: # ** # ** REQUEST_URI # ** _____________|________________ # ** / \ # ** https://fossil-scm.org/forum/info/12736b30c072551a?t=c # ** \___/ \____________/\____/\____________________/ \_/ # ** | | | | | # ** | HTTP_HOST | PATH_INFO QUERY_STRING # ** | | # ** REQUEST_SCHEMA SCRIPT_NAME # ** # Pre: # We are running in a session. # Params: # urlfile |The CGI script executable file. # urlargs |Any additional command line arguments to pass to the script. # scriptname |The component to pass as SCRIPT_NAME # pathinfo |The component to pass as PATH_INFO. # Returns: # 1 for success, or 0 for fail. # Post: # ::rivet::redirect was called. # proc execCGI {urlfile urlargs scriptname pathinfo} { variable env set ispost [expr [info exists env(REQUEST_METHOD)] && [string match "POST" $env(REQUEST_METHOD)]] # TODO: The names of variables passed should be configurable in the mycontent object. # I read through fossil/fossil-src-2.20/src/cgi.c to find the variables that fossil uses. # Copy verbatim these variables: # NOTE: I give a blank HTTP_ACCEPT_ENCODING because I want fossil to send text back. gzip # screws up the result. set envdict "" foreach key [list COMSPEC DOCUMENT_ROOT GATEWAY_INTERFACE SCGI HTTP_ACCEPT HTTP_ACCEPT_CHARSET \ HTTP_ACCEPT_LANGUAGE HTTP_ACCEPT_ENCODING HTTP_AUTHENICATION HTTP_CONNECTION \ HTTP_HOST HTTP_IF_NONE_MATCH HTTP_IF_MODIFIED_SINCE HTTP_USER_AGENT HTTP_REFERER \ QUERY_STRING REMOTE_ADDR REMOTE_PORT REMOTE_USER REQUEST_METHOD REQUEST_SCHEME \ REQUEST_URI SERVER_NAME SERVER_PROTOCOL HOME USERNAME FOSSIL_HOME USER FOSSIL_USER \ SQLITE_TMPDIR TMPDIR TEMP TMP FOSSIL_VFS FOSSIL_FORCE_TICKET_MODERATION \ FOSSIL_FORCE_WIKI_MODERATION FOSSIL_TCL_PATH REMOTE_HOST] { if {[info exists env($key)]} { dict set envdict $key $env($key) } } # We have to translate this from our main index.tcl file to the name of the script executable. dict set envdict SCRIPT_FILENAME $urlfile # 4.1.13. SCRIPT_NAME # # The SCRIPT_NAME variable MUST be set to a URI path (not URL-encoded) # which could identify the CGI script (rather than the script's # output). The syntax is the same as for PATH_INFO (section 4.1.5) # # SCRIPT_NAME = "" | ( "/" path ) # # The leading "/" is not part of the path. It is optional if the path # is NULL; however, the variable MUST still be set in that case. # # The SCRIPT_NAME string forms some leading part of the path component # of the Script-URI derived in some implementation-defined manner. No # PATH_INFO segment (see section 4.1.5) is included in the SCRIPT_NAME # value. # # TODO: SCRIPT_NAME needs to be overriden with the url of the CGI script itself? dict set envdict SCRIPT_NAME $scriptname # 4.1.5. PATH_INFO # # The PATH_INFO variable specifies a path to be interpreted by the CGI # script. It identifies the resource or sub-resource to be returned by # the CGI script, and is derived from the portion of the URI path # hierarchy following the part that identifies the script itself. # Unlike a URI path, the PATH_INFO is not URL-encoded, and cannot # contain path-segment parameters. A PATH_INFO of "/" represents a # single void path segment. # # PATH_INFO = "" | ( "/" path ) # path = lsegment *( "/" lsegment ) # lsegment = *lchar # lchar = <any TEXT or CTL except "/"> # # The value is considered case-sensitive and the server MUST preserve # the case of the path as presented in the request URI. The server MAY # impose restrictions and limitations on what values it permits for # PATH_INFO, and MAY reject the request with an error if it encounters # any values considered objectionable. That MAY include any requests # that would result in an encoded "/" being decoded into PATH_INFO, as # this might represent a loss of information to the script. Similarly, # treatment of non US-ASCII characters in the path is system-defined. # # URL-encoded, the PATH_INFO string forms the extra-path component of # the Script-URI (see section 3.3) which follows the SCRIPT_NAME part # of that path. dict set envdict PATH_INFO $pathinfo # # For POST requests we append the header strings after transforming them to upper case # with dashes changed to underscores. # Reference: FOSSIL looks for these in the environment not in stdin. # if {$ispost} { foreach key [::rivet::apache_table names headers_in] { dict set envdict [CgiHdrStrToEnv $key] [::rivet::apache_table get headers_in $key] } } # # Now construct the command line. # set cmd "/usr/bin/env" foreach key [dict keys $envdict] { append cmd " $key=[CgiExecEscape [dict get $envdict $key]]" } append cmd " $urlfile $urlargs" ::rivet::apache_log_error debug "execCGI cmd=$cmd" # # Open the CGI command as a pipeline in non blocking mode. # try { set fd [open "|$cmd" r+] fconfigure $fd -buffersize 1 -blocking 0 -translation auto } on error errmsg { ::rivet::apache_log_error err "execCGI: CGI open failed with error: $errmsg" ::rivet::apache_log_error err "execCGI: errorInfo=$::errorInfo" return 0 } # # If this is a POST request then send any data from the request body. Note that Rivet needs # to be patched with the Rivet_RawPost length change in tcl-rivet/src/mod_rivet_ng/rivetCore.c # if {$ispost} { chan configure $fd -translation binary puts -nonewline $fd [::rivet::raw_post] chan configure $fd -translation auto } # # Switch to blocking mode and read repeatedly until we've consumed all the headers, then # switch to binary mode and read the body. # set mode status chan configure $fd -translation auto -buffering line -blocking 1 set copied 0 while {![eof $fd]} { if {$mode == "status" || $mode == "header"} { # # We're in text mode, so get the next line up the the newline. # set line [gets $fd] if {$mode == "status"} { set mode "header" # Ok, some CGI scripts do not return a status code (backuppc, uptimerecords etc). # Parse the status header in the form: Status: 200 OK if {[regexp -line {^[ \t]*HTTP/[0-9]{1}.[0-9]{1}[ \t]+} $line] || [regexp -line {^[ \t]*Status:[ \t]*[0-9]{3}[ \t]+} $line]} { set reasonphrase [lassign $line statusword statuscode] ::rivet::headers numeric $statuscode ::rivet::apache_log_error debug "execCGI: Found status code: $statuscode" continue } ::rivet::apache_log_error debug "execCGI: didn't find a status code: \"$line\"" # If we didn't find a status line, then we fall through to the header handler below. } # # else ... mode == header # if {$line != ""} { # # This is a header line in the form: name1: value # if {[set colon [string first ":" $line]] < 0} { ::rivet::apache_log_error info "execCGI: Sorry I couldn't find a \":\" in this\ header statement: \"$line\"" } else { set hdr [string trim [string range $line 0 $colon-1]] set val [string trim [string range $line $colon+1 end]] if {[string match -nocase "Content-Type" $hdr]} { ::rivet::headers type $val } else { ::rivet::headers add $hdr $val } ::rivet::apache_log_error debug "execCGI: Found header \"$hdr\" with value\ \"$val\"" } } else { # # End of the headers - the rest is content. # set mode "content" # # Set the buffersize to something much larger than the ethernet frame size, # which makes the TCP fragments around frame size, otherwise apache sends # hundreds of tiny fragments (768 first effort). # fconfigure $fd -translation binary -blocking 0 -buffersize 10000 fconfigure stdout -translation binary -buffersize 10000 } } else { # # Copy all the remaining content directly to stdout. # incr copied [fcopy $fd stdout] } } catch "close $fd" ::rivet::apache_log_error debug "execCGI: Copied $copied bytes of body." return 1 } }
OpenPGP_0xEDBE43639EFD884D.asc
Description: OpenPGP public key
OpenPGP_signature
Description: OpenPGP digital signature