User:AurelioJuarez

''@aureliojuarezcortez Sign out Your account has been flagged. Because of that, your profile is hidden from the public. If you believe this is a mistake, contact support to have your account status reviewed. Watch 27 Star 174 Fork 99 metacpan/metacpan-api Code Issues 75  Pull requests 2  Projects 0  Wiki  Pulse  Graphs Branch: master Find file Copy pathmetacpan-api/docs/API-docs.md d716031 on Nov 21, 2016 @tsibley tsibley curl doesn't follow redirects by default, so specify https:// specifi… 10 contributors @oalders @rwstauner @tsibley @szabgab @ranguard @oiami @monken @haarg @dolmen @andreeap RawBlameHistory 380 lines (268 sloc) 17.5 KB API Docs: v1AurelioJuarezCortez. For an introduction to the MetaCPAN API which requires no previous knowledge of MetaCPAN or ElasticSearch, see the slides for "Abusing MetaCPAN for Fun and Profit" or watch the actual talk. There is also a repository of examples you can play with to get up and running in a hurry. Rather than editing this wiki page, please send pull requests for the metacpan-examples repository. If you'd rather edit the wiki, please do, but sending the code pull requests is probably the most helpful way to approach this. All of these URLs can be tested using the MetaCPAN Explorer To learn more about the ElasticSearch query DSL check out Clinton Gormley's [Terms of Endearment - ES Query DSL Explained] (https://www.slideshare.net/clintongormley/terms-of-endearment-the-elasticsearch-query-dsl-explained) slides. The query syntax is explained on ElasticSearch's reference page. You can also check out this getting started tutorial about Elasticsearch reference page. Being polite Currently, the only rules around using the API are to "be polite". We have enforced an upper limit of a size of 5,000 on search requests. If you need to fetch more than 5,000 items, you should look at using the scrolling API. Search this page for "scroll" to get an example using Search::Elasticsearch or see the Elasticsearch scroll docs if you are connecting in some other way. You can certainly scroll if you are fetching less than 5,000 items. You might want to do this if you are expecting a large data set, but will still need to run many requests to get all of the required data. Be aware that when you scroll, your docs will come back unsorted, as noted in the ElasticSearch scan documentation. Identifying Yourself Part of being polite is letting us know who you are and how to reach you. This is not mandatory, but please do consider adding your app to the API-Consumers page.

Available fields

Available fields can be found by accessing the corresponding _mapping endpoint.

/author/_mapping - explore /distribution/_mapping - explore /favorite/_mapping - explore /file/_mapping - explore /module/_mapping - explore /rating/_mapping - explore /release/_mapping - explore Field documentation

Fields are documented in the API codebase: https://github.com/metacpan/metacpan-api/tree/master/lib/MetaCPAN/Document Check the Pod for discussion of what the various fields represent. Be sure to have a look at https://github.com/metacpan/metacpan-api/blob/master/lib/MetaCPAN/Document/File.pm in particular as results for /module are really a thin wrapper around the file type.

Search without constraints

Performing a search without any constraints is an easy way to get sample data

/author/_search /distribution/_search /favorite/_search /file/_search /rating/_search /release/_search Joins

ElasticSearch itself doesn't support joining data across multiple types. The API server can, however, handle a join query parameter if the underlying type was set up accordingly. Browse https://github.com/metacpan/metacpan-api/blob/master/lib/MetaCPAN/Server/Controller/ to see all join conditions. Here are some examples. Joins on documents: /author/PERLER?join=favorite /author/PERLER?join=favorite&join=release /release/Moose?join=author /module/Moose?join=release Joins on search results is work in progress.

Restricting the joined results can be done by using the boolean "should" occurrence type:

curl -XPOST https://fastapi.metacpan.org/v1/author/PERLER?join=release -d ' {   "query": { "bool": { "should": [{ "term": { "release.status": "latest" }           }]        }    } }' JSONP Simply add a callback query parameter with the name of your callback, and you'll get a JSONP response. /favorite?q=distribution:Moose&callback=cb GET convenience URLs You should be able to run most POST queries, but very few GET urls are currently exposed. However, these convenience endpoints can get you started. You should note that they behave differently than the POST queries in that they will return to you the latest version of a module or dist and they remove a lot of the verbose ElasticSearch data which wraps results. /distribution/{distribution} The /distribution endpoint accepts the name of a distribution (e.g. /distribution/Moose), which returns information about the distribution which is not specific to a version (like RT bug counts). /download_url/{module} The /download_url endpoint exists specifically for the cpanm client. It takes a module name with an optional version (or range of versions) and an optional dev flag (for development releases) and returns a download_url as well as some other helpful info. Obviously anyone can use this endpoint, but we'll only consider changes to this endpoint after considering how cpanm might be affected. https://fastapi.metacpan.org/v1/download_url/HTTP::Tiny https://fastapi.metacpan.org/v1/download_url/Moose?version===0.01 https://fastapi.metacpan.org/v1/download_url/Moose?version=!=0.01 https://fastapi.metacpan.org/v1/download_url/Moose?version=<=0.02 https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.24 https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27&dev=1 https://fastapi.metacpan.org/v1/download_url/Try::Tiny?version=>0.21,<0.27,!=0.26&dev=1 /release/{distribution}

/release/{author}/{release}

The /release endpoint accepts either the name of a distribution (e.g. /release/Moose), which returns the most recent release of the distribution. Or provide the full path which consists of its author and the name of the release (e.g. /release/DOY/Moose-2.0001).

/author/{author}

author refers to the pauseid of the author. It must be uppercased (e.g. /author/DOY).

/module/{module}

Returns the corresponding file of the latest version of the module. Considering that Moose-2.0001 is the latest release, the result of /module/Moose is the same as /file/DOY/Moose-2.0001/lib/Moose.pm.

/pod/{module}

/pod/{author}/{release}/{path}

Returns the POD of the given module. You can change the output format by either passing a content-type query parameter (e.g. /pod/Moose?content-type=text/plain or by adding an Accept header to the HTTP request. Valid content types are:

text/html (default) text/plain text/x-pod text/x-markdown /source/{module}

Returns the full source of the latest, authorized version of the given module.

GET Searches

Names of latest releases by OALDERS:

https://fastapi.metacpan.org/v1/release/_search?q=author:OALDERS%20AND%20status:latest&fields=name,status&size=100

5,000 CPAN Authors:

https://fastapi.metacpan.org/v1/author/_search?q=*&size=5000

All CPAN Authors Who Have Provided Twitter IDs:

https://fastapi.metacpan.org/v1/author/_search?q=profile.name:twitter

All CPAN Authors Who Have Updated MetaCPAN Profiles:

https://fastapi.metacpan.org/v1/author/_search?q=updated:*&sort=updated:desc

First 100 distributions which SZABGAB has given a ++:

https://fastapi.metacpan.org/v1/favorite/_search?q=user:sWuxlxYeQBKoCQe1f-FQ_Q&size=100&fields=distribution

The 100 most recent releases ( similar to https://metacpan.org/recent )

https://fastapi.metacpan.org/v1/release/_search?q=status:latest&fields=name,status,date&sort=date:desc&size=100

Number of ++'es that DOY's dists have received:

https://fastapi.metacpan.org/v1/favorite/_search?q=author:DOY&size=0

List of users who have ++'ed DOY's dists and the dists they have ++'ed:

https://fastapi.metacpan.org/v1/favorite/_search?q=author:DOY&fields=user,distribution

Last 50 dists to get a ++:

https://fastapi.metacpan.org/v1/favorite/_search?size=50&fields=author,user,release,date&sort=date:desc

The Changes file of the Test-Simple distribution:

https://fastapi.metacpan.org/v1/changes/Test-Simple

Querying the API with MetaCPAN::Client

Perhaps the easiest way to get started using MetaCPAN is with MetaCPAN::Client.

You can get started with this example script to fetch author data.

Querying the API with Search::Elasticsearch

The API server at fastapi.metacpan.org is a wrapper around an Elasticsearch instance. It adds support for the convenient GET URLs, handles authentication and does some access control. Therefore you can use the powerful API of Search::Elasticsearch to query MetaCPAN.

NOTE: The cxn_pool => 'Static::NoPing' is important because of the HTTP proxy we have in front of Elasticsearch.

You can get started with this example script to fetch author data.

POST Searches

Please feel free to add queries here as you use them in your own work, so that others can learn from you.

Downstream Dependencies

This query returns a list of all releases which list MooseX::NonMoose as a dependency.

curl -XPOST https://fastapi.metacpan.org/v1/release/_search -d '{ "size": 5000, "fields": [ "distribution" ], "filter": { "and": [ { "term": { "dependency.module": "MooseX::NonMoose" } }, { "term": {"maturity": "released"} }, { "term": {"status": "latest"} } ] } }' Note it is also possible to use these queries in GET requests (useful for cross-domain JSONP requests) by appropriately encoding the JSON query into the source parameter of the URL. For example the query above would become:

curl 'https://fastapi.metacpan.org/v1/release/_search?source=%7B%22query%22%3A%7B%22match_all%22%3A%7B%7D%7D%2C%22size%22%3A5000%2C%22fields%22%3A%5B%22distribution%22%5D%2C%22filter%22%3A%7B%22and%22%3A%5B%7B%22term%22%3A%7B%22release.dependency.module%22%3A%22MooseX%3A%3ANonMoose%22%7D%7D%2C%7B%22term%22%3A%7B%22release.maturity%22%3A%22released%22%7D%7D%2C%7B%22term%22%3A%7B%22release.status%22%3A%22latest%22%7D%7D%5D%7D%7D' The size of the CPAN unpacked

Get license types of all releases in an arbitrary time span:

curl -XPOST https://fastapi.metacpan.org/v1/release/_search?size=100 -d '{ "query": { "range" : { "date" : { "gte" : "2010-06-05T00:00:00", "lte" : "2011-06-05T00:00:00" }   }  },  "fields": ["license", "name", "distribution", "date", "version_numified"] }' Aggregate by license:

curl -XPOST https://fastapi.metacpan.org/v1/release/_search -d '{ "query": { "match_all": {} },   "aggs": { "license": { "terms": { "field": "license" }       }    },    "size": 0 }' Most used file names in the root directory of releases:

curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ "query": { "filtered":{"query":{"match_all":{}},"filter":{"term":{"level":0}}} }, "aggs": { "license": { "terms": { "size":100, "field":"name" } } }, "size":0 }' Find all releases that contain a particular version of a module:

curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ "query": { "filtered":{ "query":{"match_all":{}}, "filter":{"and":[ {"term":{"module.name":"DBI::Profile"}}, {"term":{"module.version":"2.014123"}} ]} }},  "fields":["release"] }' Find all authors with Twitter in their profiles

Get a leaderboard of ++'ed distributions

Get a leaderboard of Authors with Most Uploads

Search for a release by name

Get the latest version numbers of your favorite modules

Note that "size" should be the number of distributions you are looking for.

lynx --dump --post_data https://fastapi.metacpan.org/v1/release/_search <<EOL {   "query" : { "terms" : { "distribution" : [ "Mojolicious", "MetaCPAN-API", "DBIx-Class" ] } },   "filter" : { "term" : { "status" : "latest" } }, "fields" : [ "distribution", "version" ], "size"  : 3 } EOL Get a list of all files where the directory is false and the path is blank

curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ "query": { "match_all": {} }, "size": 1000, "fields": [ "name", "status", "directory", "path", "distribution" ], "filter": { "and": [ { "term": { "directory": false } }, { "term" : { "path" : "" } } ] } }' List releases which have an email address for a bugtracker, but not an url

curl -XPOST https://fastapi.metacpan.org/v1/release/_search -d '{ "query": { "match_all": {} }, "size": 10, "fields": [ "name", "resources.bugtracker.mailto" ], "filter": { "and": [ { "term": {"maturity": "released"} }, { "term": {"status": "latest"} }, { "exists" : { "field" : "resources.bugtracker.mailto" } }, { "missing" : { "field" : "resources.bugtracker.web" } } ] } }' List distributions for which we have RT issues

Search the current PDL documentation for the string axisvals

curl -XPOST https://fastapi.metacpan.org/v1/file/_search -d '{ "query" : { "filtered" : { "query" : { "query_string" : { "query" : "axisvals", "fields" : [ "pod.analyzed", "module.name" ] } },     "filter" : { "and" : [ { "term" : { "distribution" : "PDL" } }, { "term" : { "status" : "latest" } } ]}   }},    "fields" : [ "documentation", "abstract", "module.name" ], "size" : 20 }' Contact GitHub API Training Shop Blog About © 2017 GitHub, Inc. Terms Privacy Security Status Help ￼ ￼Search Diff FELIPE / Net-WebSocket-0.01 /  FELIPE / Net-WebSocket-0.031 TOOLS Reverse diff Raw diff ￼ INFO 1509 insertions 741 deletions Top .gitignore03 Changes066 MANIFEST54 META.json712 META.yml510 Makefile.PL1453 demo/123_server.pl516 demo/echo_server.pl520 demo/lib/NWDemo.pm1521 demo/shell_server.pl0222 demo/wscat.pl135111 demo/wscat_the_hard_way.pl0281 lib/Net/WebSocket/Base/Parser.pm1770 lib/Net/WebSocket/Base/ReadFilehandle.pm940 lib/Net/WebSocket/Endpoint/Server.pm1354 lib/Net/WebSocket/Endpoint.pm7293 lib/Net/WebSocket/Frame.pm11 lib/Net/WebSocket/Handshake/Client.pm622 lib/Net/WebSocket/Handshake/Server.pm217 lib/Net/WebSocket/Mask.pm61 lib/Net/WebSocket/Message.pm11 lib/Net/WebSocket/Parser.pm33195 lib/Net/WebSocket/PingStore.pm061 lib/Net/WebSocket/RNG.pm200 lib/Net/WebSocket/Streamer.pm259 lib/Net/WebSocket/X/EmptyRead.pm140 lib/Net/WebSocket/X/ReadFilehandle.pm210 lib/Net/WebSocket.pm5290 t/parse_message.t1533 t/parse_message_close.t311 t/partial_frame.t416 t/round_trip.t926 t/single_close.t510 33 files changed (This is a file diff) 7411509 .gitignore @@ -1 +1,4 @@ *.sw* +MYMETA.* +Makefile +Net-WebSocket-* Changes @@ -0,0 +1,66 @@ +Revision history for Perl module Net::WebSocket + +0.01 Wed Mar 22 2017 +- Initial release + +0.02 Thu Mar 23 2017 +- Add this Changes file. :) + +- BREAKING: Rename Endpoint “timeout” method to “check_heartbeat”. + +- Fix missing Call::Context dependency. + +0.021 Thu Mar 23 2017 +- Remove stray Call::Always usage in tests + +0.022 Sat Apr 22 2017 +- Remove stray Try::Tiny dependency + +0.023 Sat Apr 22 2017 +- Fix another test dependency. + +0.03 Wed May 3 2017 +- BREAKING: Endpoint’s “out” parameter must now be an instance of + IO::Framed::Write (or implement the same behavior). + +- BREAKING: Parse’s parameter must now implement IO::Framed::Read. + +- BREAKING: Because we now farm the I/O out to IO::Framed +  (or whatever replacement class you may prefer), some of the thrown +  exceptions are changed: + +    Net::WebSocket::X::ReadFilehandle is gone. If you use IO::Framed, +    you’ll now receive IO::Framed::X::ReadError + +    Net::WebSocket::X::EmptyRead is gone. If you use IO::Framed, +    you’ll now receive IO::Framed::X::EmptyRead. + +- BREAKING: I’ve removed the before_send_control_frame handler. + +- Net::WebSocket::Parser now instantiates the “target” frame class object, + not the base Net::WebSocket::Frame class. So there’s no more of the + AUTOLOAD shenanigans of before. + +- Removed the Bytes::Random::Secure::Tiny dependency. It doesn’t seem we + really need cryptographically strong random numbers for masking or +  handshakes. + +- Frame I/O logic is now distributed separately as IO::Framed. It’s not + a strict dependency of this library, but you’re probably going to want + it nonetheless, unless you want to reimplement that logic yourself. + +- EINTR-trapping logic is now distributed separatedly as IO::SigGuard. + (… which is used in IO::Framed) + +- Added documentation for Net::WebSocket::Streamer + +- Refactor ping handling behavior into its own module. It could be + reusable potentially? + +- Demos now favor IO::Events. + +- Shell server demo now included. Hook it up to a nice JavaScript terminal + emulator like xterm.js, and impress all your friends. :) + +0.031 Tue May 9 2017 +- Sync documentation with 0.03 API changes MANIFEST @@ -1,17 +1,18 @@ .gitignore +Changes LICENSE MANIFEST Makefile.PL demo/123_server.pl demo/echo_server.pl demo/lib/NWDemo.pm +demo/shell_server.pl demo/wscat.pl +demo/wscat_the_hard_way.pl lib/Net/WebSocket.pm lib/Net/WebSocket/Base/ControlFrame.pm lib/Net/WebSocket/Base/DataFrame.pm lib/Net/WebSocket/Base/DataMessage.pm -lib/Net/WebSocket/Base/Parser.pm -lib/Net/WebSocket/Base/ReadFilehandle.pm lib/Net/WebSocket/Base/ReadString.pm lib/Net/WebSocket/Base/Typed.pm lib/Net/WebSocket/Constants.pm @@ -35,7 +36,7 @@ lib/Net/WebSocket/Message.pm lib/Net/WebSocket/Message/binary.pm lib/Net/WebSocket/Message/text.pm lib/Net/WebSocket/Parser.pm -lib/Net/WebSocket/RNG.pm +lib/Net/WebSocket/PingStore.pm lib/Net/WebSocket/Streamer.pm lib/Net/WebSocket/Streamer/Client.pm lib/Net/WebSocket/Streamer/Server.pm @@ -43,8 +44,6 @@ lib/Net/WebSocket/X.pm lib/Net/WebSocket/X/BadAccept.pm lib/Net/WebSocket/X/BadArg.pm lib/Net/WebSocket/X/Base.pm -lib/Net/WebSocket/X/EmptyRead.pm -lib/Net/WebSocket/X/ReadFilehandle.pm lib/Net/WebSocket/X/ReceivedBadControlFrame.pm lib/Net/WebSocket/X/ReceivedClose.pm t/create_frame.t META.json @@ -1,7 +1,7 @@ {   "abstract" : "WebSocket in Perl", "author" : [ -     "Felipe Gasper" +     "Felipe Gasper (FELIPE)" ],   "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.24, CPAN::Meta::Converter version 2.150010", @@ -32,7 +32,7 @@      },       "runtime" : { "requires" : { -           "Bytes::Random::Secure::Tiny" : "0", +           "Call::Context" : "0", "Digest::SHA" : "0", "MIME::Base64" : "0", "Module::Load" : "0", @@ -40,13 +40,18 @@            "X::Tiny" : "0", "autodie" : "0", "overload" : "0", -           "parent" : "0" +           "parent" : "0", +           "perl" : "5.014" }      },       "test" : { "requires" : { -           "Test::More" : "0", -           "Test::Simple" : "0" +           "File::Slurp" : "0", +           "File::Temp" : "0", +           "IO::Framed" : "0", +           "IO::Select" : "0", +           "Test::Deep" : "0", +           "Test::More" : "0" }      }    }, @@ -54,10 +59,10 @@    "resources" : { "repository" : { "type" : "git", -        "url" : "https://github.com/FGasper/p5-Net-WebSocket.git", +        "url" : "git://github.com/FGasper/p5-Net-WebSocket.git", "web" : "https://github.com/FGasper/p5-Net-WebSocket" }   }, -   "version" : "0.01", +  "version" : "0.031", "x_serialization_backend" : "JSON::PP version 2.27300_01" } META.yml @@ -1,11 +1,15 @@ --- abstract: 'WebSocket in Perl' author: - - 'Felipe Gasper' + - 'Felipe Gasper (FELIPE)' build_requires: ExtUtils::MakeMaker: '0' + File::Slurp: '0' + File::Temp: '0' + IO::Framed: '0' + IO::Select: '0' + Test::Deep: '0' Test::More: '0' - Test::Simple: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 1 @@ -20,7 +24,7 @@ no_index: - t    - inc requires: - Bytes::Random::Secure::Tiny: '0' + Call::Context: '0' Digest::SHA: '0' MIME::Base64: '0' Module::Load: '0' @@ -29,7 +33,8 @@ requires: autodie: '0' overload: '0' parent: '0' + perl: '5.014' resources: - repository: https://github.com/FGasper/p5-Net-WebSocket.git -version: '0.01' + repository: git://github.com/FGasper/p5-Net-WebSocket.git +version: '0.031' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Makefile.PL @@ -3,12 +3,27 @@ use ExtUtils::MakeMaker; -WriteMakefile( +WriteMakefile1( +   META_MERGE => { +       'meta-spec' => { version => 2 }, +       resources => { +           repository => { +               type => 'git', +               url => 'git://github.com/FGasper/p5-Net-WebSocket.git', +               web => 'https://github.com/FGasper/p5-Net-WebSocket', +           }, +        }, +    }, +     NAME         => 'Net::WebSocket', VERSION_FROM => 'lib/Net/WebSocket.pm',        # finds \$VERSION -   AUTHOR       => 'Felipe Gasper', +   AUTHOR       => 'Felipe Gasper (FELIPE)', ABSTRACT_FROM => 'lib/Net/WebSocket.pm', LICENSE     => 'perl', + +   #Bleh. Hopefully can improve. +   MIN_PERL_VERSION => 5.014000, +    PREREQ_PM    => { 'autodie'                 => 0, 'parent'                  => 0, @@ -18,20 +33,44 @@ WriteMakefile(        'MIME::Base64' => 0,         'Digest::SHA' => 0,         'URI::Split' => 0, -        'Bytes::Random::Secure::Tiny' => 0, -    }, -    META_MERGE => { -        'meta-spec' => { version => 2 }, -        resources   => { -            repository => { -                type => 'git', -                url  => 'https://github.com/FGasper/p5-Net-WebSocket.git', -                web  => 'https://github.com/FGasper/p5-Net-WebSocket', -            }, -        }, +        'Call::Context' => 0,     }, +     TEST_REQUIRES => {         'Test::More'      => 0, -        'Test::Simple'    => 0, +        'Test::Deep' => 0, +        'File::Temp' => 0, +        'File::Slurp' => 0, +        'IO::Framed' => 0, +        'IO::Select' => 0,     }, ); + +sub WriteMakefile1 { #Compatibility code for old versions of EU::MM. Written by Alexandr Ciornii, version 2. Added by eumm-upgrade. +   my %params=@_; +   my $eumm_version=$ExtUtils::MakeMaker::VERSION; +   $eumm_version=eval $eumm_version; +   die "EXTRA_META is deprecated" if exists $params{EXTRA_META}; +   die "License not specified" if not exists $params{LICENSE}; +   if ($params{AUTHOR} and ref($params{AUTHOR}) eq 'ARRAY' and $eumm_version < 6.5705) { +       $params{META_ADD}->{author}=$params{AUTHOR}; +       $params{AUTHOR}=join(', ',@{$params{AUTHOR}}); +   } +    if ($params{TEST_REQUIRES} and $eumm_version < 6.64) { +       $params{BUILD_REQUIRES}={ %{$params{BUILD_REQUIRES} || {}}, %{$params{TEST_REQUIRES}} }; +       delete $params{TEST_REQUIRES}; +   } +    if ($params{BUILD_REQUIRES} and $eumm_version < 6.5503) { +       #EUMM 6.5502 has problems with BUILD_REQUIRES +       $params{PREREQ_PM}={ %{$params{PREREQ_PM} || {}}, %{$params{BUILD_REQUIRES}} }; +       delete $params{BUILD_REQUIRES}; +   } +    delete $params{CONFIGURE_REQUIRES} if $eumm_version < 6.52; +   delete $params{MIN_PERL_VERSION} if $eumm_version < 6.48; +   delete $params{META_MERGE} if $eumm_version < 6.46; +   delete $params{META_ADD} if $eumm_version < 6.46; +   delete $params{LICENSE} if $eumm_version < 6.31; + +   WriteMakefile(%params); +} + demo/123_server.pl @@ -35,6 +35,13 @@ my $server = IO::Socket::INET->new(    Listen => 2, ); +#This is a “lazy” example. A more robust, production-level +#solution would not need to fork unless there were privilege +#drops or some such that necessitate separate processes per session. + +#For an example of a non-forking server in Perl, look at Net::WAMP’s +#router example. + while ( my $sock = $server->accept ) { fork and next; @@ -62,16 +69,20 @@ while ( my $sock = $server->accept ) { my $cur_number = 0; while (!$ept->is_closed) { -       my ( $rdrs_ar, $s, $errs_ar ) = IO::Select->select( $s, $s, $s, 1 ); +       my $write_s = $ept->get_write_queue_size ? $s : undef; + +       my ( $rdrs_ar, $wtrs_ar, $errs_ar ) = IO::Select->select( $s, $write_s, $s, 1 ); my $is_final = ($cur_number == 2); my $method = $is_final ? 'create_final' : 'create_chunk'; -       syswrite( -            $sock, -            $streamer->$method($cur_number)->to_bytes, -        ); +       if ( $wtrs_ar && @$wtrs_ar ) { +           syswrite( +                $sock, +                $streamer->$method($cur_number)->to_bytes, +            ); +       }         $cur_number++; $cur_number %= 3; demo/echo_server.pl @@ -6,6 +6,8 @@ use autodie; use Try::Tiny; +use lib '/Users/Felipe/code/p5-IO-SigGuard/lib'; + use IO::Socket::INET ; use IO::Select ; @@ -37,6 +39,13 @@ my $server = IO::Socket::INET->new(    Listen => 2, ); +#This is a “lazy” example. A more robust, production-level +#solution would not need to fork unless there were privilege +#drops or some such that necessitate separate processes per session. + +#For an example of a non-forking server in Perl, look at Net::WAMP’s +#router example. + while ( my $sock = $server->accept ) { fork and next; @@ -72,21 +81,27 @@ while ( my $sock = $server->accept ) { },    ); +    my $write_select = IO::Select->new($sock); +     while (!$ept->is_closed) { -        my ( $rdrs_ar, undef, $errs_ar ) = IO::Select->select( $s, undef, $s, 10 ); +        my $cur_write_s = $ept->get_write_queue_size ? $write_select : undef; + +        my ( $rdrs_ar, $wtrs_ar, $errs_ar ) = IO::Select->select( $s, $cur_write_s, $s, 10 ); + +        $ept->process_write_queue if $wtrs_ar && @$wtrs_ar; -        if ($errs_ar && @$errs_ar) { +        if (@$errs_ar) {             $s->remove($sock);             last;         } -        if (!$rdrs_ar && !$errs_ar) { -            $ept->timeout; +        if (!@$rdrs_ar && !($wtrs_ar && @$wtrs_ar) && !@$errs_ar) { +            $ept->check_heartbeat;             last if $ept->is_closed;             next;         } -        if ( $rdrs_ar ) { +        if ( @$rdrs_ar ) {             try {                 $ept->get_next_message; } demo/lib/NWDemo.pm @@ -6,6 +6,8 @@ use autodie; use HTTP::Request ; +use IO::SigGuard ; + use Net::WebSocket::Handshake::Server ; use Net::WebSocket::Frame::close ; @@ -13,21 +15,14 @@ use constant MAX_CHUNK_SIZE => 64000; use constant CRLF => "\x0d\x0a"; -sub handshake_as_server { -   my ($inet) = @_; - -   my $buf = q<>; - -   #Read the server handshake. -   my $idx; -   while ( sysread $inet, $buf, MAX_CHUNK_SIZE, length $buf ) { -       $idx = index($buf, CRLF . CRLF); -       last if -1 != $idx; -   } +#Shortens the given text. +sub get_server_handshake_from_text { +   my $idx = index($_[0], CRLF . CRLF); +   return undef if -1 == $idx; -   my $hdrs_txt = substr( $buf, 0, $idx + 2 * length(CRLF), q<> ); +   my $hdrs_txt = substr( $_[0], 0, $idx + 2 * length(CRLF), q<> ); -   die "Extra garbage! ($buf)" if length $buf; +   die "Extra garbage! ($_[0])" if length $_[0]; my $req = HTTP::Request->parse($hdrs_txt); @@ -38,11 +33,22 @@ sub handshake_as_server { my $key = $req->header('Sec-WebSocket-Key'); -   my $handshake = Net::WebSocket::Handshake::Server->new( +    return Net::WebSocket::Handshake::Server->new( key => $key, ); +} + +sub handshake_as_server { +   my ($inet) = @_; + +    my $buf = q<>; +    my $hsk; +    while ( IO::SigGuard::sysread($inet, $buf, MAX_CHUNK_SIZE, length $buf ) ) { +        $hsk = get_server_handshake_from_text($buf); +        last if $hsk; +    } -    print { $inet } $handshake->create_header_text . CRLF; +    print { $inet } $hsk->create_header_text . CRLF;     return; } demo/shell_server.pl @@ -0,0 +1,222 @@ +#!/usr/bin/env perl + +#-- +# No guarantees are made as to the robustness of this server code; +# this is meant purely as a demonstration of something cool to do with +# WebSocket. :) +#-- + +use strict; +use warnings; +use autodie; + +use Try::Tiny; + +use Socket; + +use IO::Events ; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use lib "$FindBin::Bin/lib"; +use NWDemo ; + +use Net::WebSocket::Endpoint::Server ; +use Net::WebSocket::Frame::text ; +use Net::WebSocket::Frame::binary ; +use Net::WebSocket::Frame::continuation ; +use Net::WebSocket::Handshake::Server ; +use Net::WebSocket::Parser ; + +use IO::Pty ; + +#for setsid +use POSIX ; + +my $host_port = $ARGV[0] || die "Need host:port or port!\n"; + +if (index($host_port, ':') == -1) { +   substr( $host_port, 0, 0 ) = '127.0.0.1:'; +} + +my ($host, $port) = split m<:>, $host_port; + +my $loop = IO::Events::Loop->new; + +my %sessions; + +sub _kill_session { +   my ($session) = @_; + +   if ( $sessions{$session} ) { +       $sessions{$session}{'timeout'}->stop; + +       $sessions{$session}{'client'}->destroy; + +       if ($sessions{$session}{'shell'}) { +           $sessions{$session}{'shell'}->destroy; +       } + +        delete $sessions{$session}; +   } +} + +my $server = IO::Events::Socket::TCP->new( +    owner => $loop, +    listen => 1, +    addr => $host, +    port => $port, +    on_read => sub { +        my $shell = (getpwuid $>)[8] or die "No shell!"; + +        my $session = rand; + +        my $did_handshake; +        my $read_buffer = q<>; +        open my $rfh, '<', \$read_buffer; + +        my $ept = Net::WebSocket::Endpoint::Server->new( +           parser => Net::WebSocket::Parser->new($rfh), +       ); + +        my $shell_hdl; + +        my $client_hdl; + +        my $cpid; + +        my $timeout = IO::Events::Timer->new( +           owner => $loop, +           timeout => 5, +           repetitive => 1, +           active => 1, +           on_tick => sub { +               if ($did_handshake) { +                   $ept->check_heartbeat; + +                   if ($ept->is_closed) { +                       $shell_hdl->destroy;  #kills PID and $client_hdl +                   } +                    else { + +                       #Handle any control frames we might need to write out, +                       #esp. pings. +                       while ( my $frame = $ept->shift_write_queue ) { +                           $client_hdl->write($frame->to_bytes); +                       } +                    } +                } +                else { +                   _kill_session($session); +               } +            }, +        ); + +        $client_hdl = shift->accept( +           owner => $loop, +           read => 1, +           write => 1, +           on_close => sub { +               _kill_session($session); +           }, +            on_read => sub { +               my ($client_hdl) = @_; + +               $read_buffer .= $client_hdl->read; + +               if ($did_handshake) { +                   if (my $msg = $ept->get_next_message) { + +                       #printf STDERR "from client: %s\n", ($msg->get_payload =~ s<([\x80-\xff])>gre); +                       #printf STDERR "from client: %v.02x\n", $msg->get_payload; + +                       $shell_hdl->write( $msg->get_payload ); +                   } + +                    while (my $frame = $ept->shift_write_queue) { +                       $client_hdl->write($frame->to_bytes); +                   } +                } +                else { +                   my $hsk = NWDemo::get_server_handshake_from_text($read_buffer); +                   return if !$hsk; + +                   #-- + +                    $client_hdl->write( $hsk->create_header_text . "\x0d\x0a" ); +                   $did_handshake = 1; + +                   my $pty = IO::Pty->new; + +                   $cpid = fork or do { +                       eval { +                           my $slv = $pty->slave; +                           open \*STDIN, '<&=', $slv; +                           open \*STDOUT, '>&=', $slv; +                           open \*STDERR, '>&=', $slv; + +                           #Necessary for CTRL-C and CTRL-\ to work. +                           POSIX::setsid; + +                           #Any advantage to these?? +                           #setpgrp; +                           #$pty->make_slave_controlling_terminal; + +                           #Dunno if all shells have a “--login” switch … +                           exec { $shell } $shell, '--login' or die $!; +                       }; +                        warn if $@; +                       POSIX::exit(1); +                   }; + +                    $shell_hdl = IO::Events::Handle->new( +                        owner => $loop, +                        handle => $pty, +                        read => 1, +                        write => 1, + +                        on_read => sub { +                            my ($self) = @_; +                            my $frame = Net::WebSocket::Frame::text->new( +                               payload_sr => \$self->read, +                           ); + +                            #printf STDERR "to client: %s\n", ($frame->to_bytes =~ s<([\x80-\xff])>gre); +                            #printf STDERR "to client: %v.02x\n", $frame->get_payload; + +                            $client_hdl->write($frame->to_bytes); +                        }, + +                        pid => $cpid, + +                        on_close => sub { +                            _kill_session($session); +                        }, +                    ); + +                   $sessions{$session}{'shell'} = $shell_hdl; +               } +            } +        ); + +        $sessions{$session} = { +            client => $client_hdl, +            timeout => $timeout, +        }; +    }, +); + +while (1) { +   try { +       $loop->yield while 1; +   } +    catch { +       if ( !try { $_->isa('Net::WebSocket::X::ReceivedClose') } ) { +           local $@ = $_; +           die; +       } + +        $loop->flush; +   }; +} demo/wscat.pl @@ -6,8 +6,9 @@ use autodie; use Try::Tiny; +use IO::Events ; + use HTTP::Response; -use IO::Select ; use IO::Socket::INET ; use Socket ; use URI::Split ; @@ -25,6 +26,7 @@ use constant MAX_CHUNK_SIZE => 64000; use constant CRLF => "\x0d\x0a"; +#No PIPE use constant ERROR_SIGS => qw( INT HUP QUIT ABRT USR1 USR2 SEGV ALRM TERM ); run( @ARGV ) if !caller; @@ -32,6 +34,8 @@ run( @ARGV ) if !caller; sub run { my ($uri) = @_; +   -t \*STDIN or die "STDIN must be a TTY for this demo.\n"; +    my ($uri_scheme, $uri_authority) = URI::Split::uri_split($uri); if (!$uri_scheme) { @@ -70,182 +74,154 @@ sub run { die "Unknown scheme ($uri_scheme) in URI: “$uri”"; } -   my $buf_sr = _handshake_as_client( $inet, $uri ); +   my $loop = IO::Events::Loop->new; -   _mux_after_handshake( \*STDIN, \*STDOUT, $inet, $$buf_sr ); - -   exit 0; -} +   my ($handle, $ept); -sub _handshake_as_client { -   my ($inet, $uri) = @_; +   my $timeout = IO::Events::Timer->new( +        owner => $loop, +        timeout => 5, +        repetitive => 1, +        on_tick => sub { +            $ept->check_heartbeat; -    my $handshake = Net::WebSocket::Handshake::Client->new( -       uri => $uri, +           #Handle any control frames we might need to write out, +           #esp. pings. +           while ( my $frame = $ept->shift_write_queue ) { +               $handle->write($frame->to_bytes); +           } +        },     ); -    my $hdr = $handshake->create_header_text; +    my @to_write; -    #Write out the client handshake. -    syswrite( $inet, $hdr. CRLF ); +   my $sent_handshake; +    my $got_handshake; -    my $handshake_ok; +    my $read_buffer = q<>; -    my $buf = q<>; +    open my $read_fh, '<', \$read_buffer; -    #Read the server handshake. -    my $idx; -    while ( sysread $inet, $buf, MAX_CHUNK_SIZE, length $buf ) { -        $idx = index($buf, CRLF. CRLF); -       last if -1 != $idx; -    } +    my $handshake; -    my $hdrs_txt = substr( $buf, 0, $idx + 2 * length(CRLF), q<> ); +    $handle = IO::Events::Handle->new( +       owner => $loop, +       handle => $inet, +       read => 1, +       write => 1, -   my $req = HTTP::Response->parse($hdrs_txt); +       on_create => sub { +           my ($self) = @_; -   my $code = $req->code; -   die "Must be 101, not “$code”" if $code != 101; +           $timeout->start; -   my $upg = $req->header('upgrade'); -   $upg =~ tr; -   die "“Upgrade” must be “websocket”, not “$upg”!" if $upg ne 'websocket'; +           $handshake = Net::WebSocket::Handshake::Client->new( +                uri => $uri, +            ); -   my $conn = $req->header('connection'); -   $conn =~ tr; -   die "“Upgrade” must be “upgrade”, not “$conn”!" if $conn ne 'upgrade'; +           my $hdr = $handshake->create_header_text; -   my $accept = $req->header('Sec-WebSocket-Accept'); -   $handshake->validate_accept_or_die($accept); +           $self->write( $hdr . CRLF ); -   return \$buf; -} +           $sent_handshake = 1; +       }, -my $sent_ping; +       on_read => sub { +           my ($self) = @_; -sub _mux_after_handshake { -   my ($from_caller, $to_caller, $inet, $buf) = @_; +           $read_buffer .= $self->read; -   my $parser = Net::WebSocket::Parser->new( -        $inet, -        $buf, -    ); +           if (!$got_handshake) { +               my $idx = index($read_buffer, CRLF . CRLF); +               return if -1 == $idx; -   my $ept = Net::WebSocket::Endpoint::Client->new( -        out => $inet, -        parser => $parser, -    ); +               my $hdrs_txt = substr( $read_buffer, 0, $idx + 2 * length(CRLF), q<> ); -   for my $sig (ERROR_SIGS) { -       $SIG{$sig} = sub { -           my ($the_sig) = @_; +               my $req = HTTP::Response->parse($hdrs_txt); -           my $code = ($the_sig eq 'INT') ? 'SUCCESS' : 'ENDPOINT_UNAVAILABLE'; +               my $code = $req->code; +               die "Must be 101, not “$code”" if $code != 101; -           my $frame = Net::WebSocket::Frame::close->new( -                code => $code, -                mask => Net::WebSocket::Mask::create, -            ); +               my $upg = $req->header('upgrade'); +               $upg =~ tr; +               die "“Upgrade” must be “websocket”, not “$upg”!" if $upg ne 'websocket'; -           syswrite( $inet, $frame->to_bytes ); +               my $conn = $req->header('connection'); +               $conn =~ tr; +               die "“Upgrade” must be “upgrade”, not “$conn”!" if $conn ne 'upgrade'; -           $SIG{$the_sig} = 'DEFAULT'; +               my $accept = $req->header('Sec-WebSocket-Accept'); +               $handshake->validate_accept_or_die($accept); -           kill $the_sig, $$; -       }; -    } +                $got_handshake = 1; +           } + +            $ept ||= Net::WebSocket::Endpoint::Client->new( +                out => $inet, +                parser => Net::WebSocket::Parser->new( $read_fh ), +            ); + +           if (my $msg = $ept->get_next_message) { +               my $payload = $msg->get_payload; +               syswrite( \*STDOUT, substr( $payload, 0, 64, q<> ) ) while length $payload; +           } -    if ( -t $from_caller ) { -       $_->blocking(0) for ($from_caller, $inet); +           #Handle any control frames we might need to write out. +           while ( my $frame = $ept->shift_write_queue ) { +               $self->write($frame->to_bytes); +           } -        my $s = IO::Select->new( $from_caller, $inet ); +           $timeout->start; +       }, +    ); -        while (1) { -            my ($rdrs_ar, undef, $excs_ar) = IO::Select->select( $s, undef, $s, 10 ); +    my $closed; -            for my $err (@$excs_ar) { -                $s->remove($err); +    my $stdin = IO::Events::stdin->new( +       owner => $loop, +       read => 1, +       on_read => sub { +           my ($self) = @_; -               if ($err == $inet) { -                   warn "Error in socket reader!"; -               } -                elsif ($err == $from_caller) { -                   warn "Error in input reader!"; -               } -                else { -                   die "Improper select error: [$err]"; -               } -            } +            my $frame = Net::WebSocket::Frame::binary->new( +                payload_sr => \$self->read, +                mask => Net::WebSocket::Mask::create, +            ); -           for my $rdr (@$rdrs_ar) { -               if ($rdr == $from_caller) { -                   sysread $from_caller, my $buf, 32768; -                   _chunk_to_remote($buf, $inet); -               } -                elsif ($rdr == $inet) { -                   if ( my $msg = $ept->get_next_message ) { -                       syswrite( $to_caller, $msg->get_payload ); -                   } -                } -                else { -                   die "Improper reader: [$rdr]"; -               } -            } +            $handle->write($frame->to_bytes); +       }, -            if (!$rdrs_ar && !$excs_ar) { -               $ept->timeout; -               last if $ept->is_closed; -           } -        } -    } -    else { -       while ( sysread $from_caller, my $buf, 32768 ) { -           _chunk_to_remote( $buf, $inet ); -       } +        on_close => sub { +           $closed = 1; +       }, -        my $close_frame = Net::WebSocket::Frame::close->new( -            code => 'SUCCESS', -            mask => Net::WebSocket::Mask::create, -        ); +       on_error => sub { +           print STDERR "ERROR\n"; +       }, +    ); -        syswrite( $inet, $close_frame->to_bytes ); +    for my $sig (ERROR_SIGS) { +        $SIG{$sig} = sub { +            my ($the_sig) = @_; -        shutdown $inet, Socket::SHUT_WR; +            my $code = ($the_sig eq 'INT') ? 'SUCCESS' : 'ENDPOINT_UNAVAILABLE'; -        try { -            while ( my $msg = $ept->get_next_message ) { -                syswrite( $to_caller, $msg->get_payload ); -            } -        } -        catch { -            my $ok; -            if ( try { $_->isa('Net::WebSocket::X::ReceivedClose') } ) { -                if ( $_->get('frame')->get_payload eq $close_frame->get_payload ) { -                    $ok = 1; -                } +            $ept->shutdown( code => $code ); + +            while ( my $frame = $ept->shift_write_queue ) { +                $handle->write($frame->to_bytes);             } -            warn $_ if !$ok; -       }; +            local $SIG{'PIPE'} = 'IGNORE'; +           $handle->flush; -       close $inet; +           $SIG{$the_sig} = 'DEFAULT'; -       close $from_caller; +           kill $the_sig, $$; +       };     } -    return; -} - -sub _chunk_to_remote { -   my ($buf, $out_fh) = @_; - -   syswrite( -        $out_fh, -        Net::WebSocket::Frame::binary->new( -           payload_sr => \$buf, -           mask => Net::WebSocket::Mask::create, -       )->to_bytes, -    ); +   $loop->yield while !$closed; -   return; +   $loop->flush; } demo/wscat_the_hard_way.pl @@ -0,0 +1,281 @@ +#!/usr/bin/env perl + +#-- +# You probably should use an event loop rather than futzing with +# multiplexing manually. But if you feel you must, here is an example +# of how Net::WebSocket can work in that context. +# +# Still, please look at wscat.pl for an illustration how much of the +# “mess” an event loop can take on for you. +#-- + +use strict; +use warnings; +use autodie; + +use Try::Tiny; + +use lib '/Users/Felipe/code/p5-IO-SigGuard/lib'; + +use HTTP::Response; +use IO::Select ; +use IO::Socket::INET ; +use Socket ; +use URI::Split ; + +use IO::SigGuard ; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use Net::WebSocket::Endpoint::Client ; +use Net::WebSocket::Frame::binary ; +use Net::WebSocket::Frame::close ; +use Net::WebSocket::Handshake::Client ; +use Net::WebSocket::Parser ; + +use constant MAX_CHUNK_SIZE => 64000; + +use constant CRLF => "\x0d\x0a"; + +use constant ERROR_SIGS => qw( INT HUP QUIT ABRT USR1 USR2 SEGV ALRM TERM ); + +run( @ARGV ) if !caller; + +sub run { +   my ($uri) = @_; + +   my ($uri_scheme, $uri_authority) = URI::Split::uri_split($uri); + +   if (!$uri_scheme) { +       die "Need a URI!\n"; +   } + +    if ($uri_scheme !~ m<\Awss?\z>) { +       die sprintf "Invalid schema: “%s” ($uri)\n", $uri_scheme; +   } + +    my $inet; + +   my ($host, $port) = split m<:>, $uri_authority; + +   if ($uri_scheme eq 'ws') { +       my $iaddr = Socket::inet_aton($host); + +       $port ||= 80; +       my $paddr = Socket::pack_sockaddr_in( $port, $iaddr ); + +       socket( $inet, Socket::PF_INET, Socket::SOCK_STREAM, 0 ); +       connect( $inet, $paddr ); +   } +    elsif ($uri_scheme eq 'wss') { +       require IO::Socket::SSL; + +       $inet = IO::Socket::SSL->new( +            PeerHost => $host, +            PeerPort => $port || 443, +            SSL_hostname => $host, +        ); + +       die "IO::Socket::SSL: [$!][$@]\n" if !$inet; +   } +    else { +       die "Unknown scheme ($uri_scheme) in URI: “$uri”"; +   } + +    my $buf_sr = _handshake_as_client( $inet, $uri ); + +   _mux_after_handshake( \*STDIN, \*STDOUT, $inet, $$buf_sr ); + +   exit 0; +} + +sub _handshake_as_client { +   my ($inet, $uri) = @_; + +   my $handshake = Net::WebSocket::Handshake::Client->new( +        uri => $uri, +    ); + +   my $hdr = $handshake->create_header_text; + +   #Write out the client handshake. +   IO::SigGuard::syswrite( $inet, $hdr . CRLF ); + +   my $handshake_ok; + +   my $buf = q<>; + +   #Read the server handshake. +   my $idx; +   while ( IO::SigGuard::sysread($inet, $buf, MAX_CHUNK_SIZE, length $buf ) ) { +       $idx = index($buf, CRLF . CRLF); +       last if -1 != $idx; +   } + +    my $hdrs_txt = substr( $buf, 0, $idx + 2 * length(CRLF), q<> ); + +   my $req = HTTP::Response->parse($hdrs_txt); + +   my $code = $req->code; +   die "Must be 101, not “$code”" if $code != 101; + +   my $upg = $req->header('upgrade'); +   $upg =~ tr; +   die "“Upgrade” must be “websocket”, not “$upg”!" if $upg ne 'websocket'; + +   my $conn = $req->header('connection'); +   $conn =~ tr; +   die "“Upgrade” must be “upgrade”, not “$conn”!" if $conn ne 'upgrade'; + +   my $accept = $req->header('Sec-WebSocket-Accept'); +   $handshake->validate_accept_or_die($accept); + +   return \$buf; +} + +my $sent_ping; + +sub _mux_after_handshake { +   my ($from_caller, $to_caller, $inet, $buf) = @_; + +   my $parser = Net::WebSocket::Parser->new( +        $inet, +        $buf, +    ); + +   for my $sig (ERROR_SIGS) { +       $SIG{$sig} = sub { +           my ($the_sig) = @_; + +           my $code = ($the_sig eq 'INT') ? 'SUCCESS' : 'ENDPOINT_UNAVAILABLE'; + +           my $frame = Net::WebSocket::Frame::close->new( +                code => $code, +                mask => Net::WebSocket::Mask::create, +            ); + +           IO::SigGuard::syswrite( $inet, $frame->to_bytes ); + +           $SIG{$the_sig} = 'DEFAULT'; + +           kill $the_sig, $$; +       }; +    } + +    if ( -t $from_caller ) { +       $_->blocking(0) for ($from_caller, $inet); + +       #start it as non-blocking +       my $ept = Net::WebSocket::Endpoint::Client->new( +            out => $inet, +            parser => $parser, +        ); + +       my $s = IO::Select->new( $from_caller, $inet ); +       my $write_s = IO::Select->new($inet); + +       while (1) { +           my $cur_write_s = $ept->get_write_queue_size ? $write_s : undef; + +           #This is a really short timeout, btw. +           my ($rdrs_ar, $wtrs_ar, $excs_ar) = IO::Select->select( $s, $cur_write_s, $s, 3 ); + +           #There’s only one possible. +           if ($wtrs_ar && @$wtrs_ar) { +               $ept->process_write_queue; +           } + +            for my $err (@$excs_ar) { +               $s->remove($err); + +               if ($err == $inet) { +                   warn "Error in socket reader!"; +               } +                elsif ($err == $from_caller) { +                   warn "Error in input reader!"; +               } +                else { +                   die "Improper select error: [$err]"; +               } +            } + +            for my $rdr (@$rdrs_ar) { +               if ($rdr == $from_caller) { +                   IO::SigGuard::sysread( $from_caller, my $buf, 32768 ); +                   _chunk_to_remote($buf, $inet); +               } +                elsif ($rdr == $inet) { +                   if ( my $msg = $ept->get_next_message ) { +                       IO::SigGuard::syswrite( $to_caller, $msg->get_payload ); +                   } +                } +                else { +                   die "Improper reader: [$rdr]"; +               } +            } + +            if (!@$rdrs_ar && !($wtrs_ar && @$wtrs_ar) && !@$excs_ar) { +               $ept->check_heartbeat; +               last if $ept->is_closed; +           } +        } +    } +    else { + +       #blocking +       my $ept = Net::WebSocket::Endpoint::Client->new( +            out => $inet, +            parser => $parser, +        ); + +       while ( IO::SigGuard::sysread($from_caller, my $buf, 32768 ) ) { +           _chunk_to_remote( $buf, $inet ); +       } + +        my $close_frame = Net::WebSocket::Frame::close->new( +            code => 'SUCCESS', +            mask => Net::WebSocket::Mask::create, +        ); + +       IO::SigGuard::syswrite( $inet, $close_frame->to_bytes ); + +       shutdown $inet, Socket::SHUT_WR; + +       try { +           while ( my $msg = $ept->get_next_message ) { +               IO::SigGuard::syswrite( $to_caller, $msg->get_payload ); +           } +        } +        catch { +           my $ok; +           if ( try { $_->isa('Net::WebSocket::X::ReceivedClose') } ) { +               if ( $_->get('frame')->get_payload eq $close_frame->get_payload ) { +                   $ok = 1; +               } +            } + +            warn $_ if !$ok; +       }; + +        close $inet; + +       close $from_caller; +   } + +    return; +} + +sub _chunk_to_remote { +   my ($buf, $out_fh) = @_; + +   IO::SigGuard::syswrite( +        $out_fh, +        Net::WebSocket::Frame::binary->new( +           payload_sr => \$buf, +           mask => Net::WebSocket::Mask::create, +       )->to_bytes, +    ); + +   return; +} lib/Net/WebSocket/Base/Parser.pm @@ -1,177 +0,0 @@ -package Net::WebSocket::Base::Parser; - -use strict; -use warnings; - -use Module::Load ; - -use Net::WebSocket::Constants ; -use Net::WebSocket::X ; - -use constant { -   OPCODE_CLASS_0 => 'Net::WebSocket::Frame::continuation', -   OPCODE_CLASS_1 => 'Net::WebSocket::Frame::text', -   OPCODE_CLASS_2 => 'Net::WebSocket::Frame::binary', -   OPCODE_CLASS_8 => 'Net::WebSocket::Frame::close', -   OPCODE_CLASS_9 => 'Net::WebSocket::Frame::ping', -   OPCODE_CLASS_10 => 'Net::WebSocket::Frame::pong', -}; - -sub get_next_frame { -   my ($self) = @_; - -   if (!exists $self->{'_buffer'}) { -       $self->{'_buffer'} = q<>; -   } - -    #It is really, really inconvenient that Perl has no “or” operator -   #that considers q<> falsey but '0' truthy. :-/ -   #That aside, if indeed all we read is '0', then we know that’s not -   #enough, and we can return. -   my $first2 = $self->_read_with_buffer(2) or return undef; - -   #Now that we’ve read our header bytes, we’ll read some more. -   #There may not actually be anything to read, though, in which case -   #some readers will error (e.g., EAGAIN from a non-blocking filehandle). -   #From a certain ideal we’d return #on each individual read to allow -   #the reader to wait until there is more data ready; however, for -   #practicality (and speed) let’s go ahead and try to read the rest of -    #the frame. That means we need to set some flag to let the reader know -   #not to die if there’s no more data currently, as we’re probably -   #expecting more soon to complete the frame. -   local $self->{'_reading_frame'} = 1; - -   my ($oct1, $oct2) = unpack('CC', $first2 ); - -   my $len = $oct2 & 0x7f; - -   my $mask_size = ($oct2 & 0x80) && 4; - -   my $len_len = ($len == 0x7e) ? 2 : ($len == 0x7f) ? 8 : 0; -   my $len_buf = q<>; - -   my ($longs, $long); - -   if ($len_len) { -       $len_buf = $self->_read_with_buffer($len_len) or do { -           substr( $self->{'_buffer'}, 0, 0, $first2 ); -           return undef; -       }; - -        if ($len_len == 2) { -           ($longs, $long) = ( 0, unpack('n', $len_buf) ); -       } -        else { -           ($longs, $long) = ( unpack('NN', $len_buf) ); -       } -    } -    else { -       ($longs, $long) = ( 0, $len ); -   } - -    my $mask_buf; -   if ($mask_size) { -       $mask_buf = $self->_read_with_buffer($mask_size) or do { -           substr( $self->{'_buffer'}, 0, 0, $first2 . $len_buf ); -           return undef; -       }; -    } -    else { -       $mask_buf = q<>; -   } - -    my $payload = q<>; - -   for ( 1 .. $longs ) { - -       #32-bit systems don’t know what 2**32 is. -       #MacOS, at least, also chokes on sysread( 2**31, … ) -       #(Is their size_t signed??), even on 64-bit. -       for ( 1 .. 4 ) { -           $self->_append_chunk( 2**30, \$payload ) or do { -               substr( $self->{'_buffer'}, 0, 0, $first2 . $len_buf . $mask_buf . $payload ); -               return undef; -           }; -        } -    } - -    if ($long) { -       $self->_append_chunk( $long, \$payload ) or do { -           substr( $self->{'_buffer'}, 0, 0, $first2 . $len_buf . $mask_buf . $payload ); -           return undef; -       }; -    } - -    $self->{'_buffer'} = q<>; - -   my $opcode = $oct1 & 0xf; - -   my $frame_class = $self->{'_opcode_class'}{$opcode} ||= do { -       my $class; -       if (my $cr = $self->can("OPCODE_CLASS_$opcode")) { -           $class = $cr->; -       } -        else { -           die "$self: Unrecognized frame opcode: “$opcode”"; -       } - -        if (!$class->can('new')) { -           Module::Load::load($class); -       } - -        $class; -   }; - -    return $frame_class->create_from_parse(\$first2, \$len_len, \$mask_buf, \$payload); -} - -#sub has_partial_frame { -#   my ($self) = @_; -# -#   return length($self->{'_buffer'}) ? 1 : 0; -#} - -#This will only return exactly the number of bytes requested. -#If fewer than we want are available, then we return undef. -sub _read_with_buffer { -   my ($self, $length) = @_; - -   #Prioritize the case where we read everything we need. - -   if ( length($self->{'_buffer'}) < $length ) { -       my $deficit = $length - length($self->{'_buffer'}); -       my $read = $self->_read($deficit); - -       if (length($read) < $deficit) { -           $self->{'_buffer'} .= $read; -           return undef; -       } - -        return substr($self->{'_buffer'}, 0, length($self->{'_buffer'}), q<>). $read; -   } - -    return substr( $self->{'_buffer'}, 0, $length, q<> ); -} - -sub _append_chunk { -   my ($self, $length, $buf_sr) = @_; - -   my $start_buf_len = length $$buf_sr; - -   my $cur_buf; - -   while (1) { -       my $read_so_far = length($$buf_sr) - $start_buf_len; - -       $cur_buf = $self->_read_with_buffer($length - $read_so_far); -       return undef if !defined $cur_buf; - -       $$buf_sr .= $cur_buf; - -       last if (length($$buf_sr) - $start_buf_len) >= $length; -   } - -    return 1; -} - -1; lib/Net/WebSocket/Base/ReadFilehandle.pm @@ -1,94 +0,0 @@ -package Net::WebSocket::Base::ReadFilehandle; - -use strict; -use warnings; - -use Try::Tiny; - -use Net::WebSocket::X ; - -sub new { -   my ($class, $fh, $start_buf) = @_; - -   die "Need filehandle!" if !UNIVERSAL::isa($fh, 'GLOB'); - -   if (!length $start_buf) { -       $start_buf = q<>; -   } - -    #Determine if this is an OS-level filehandle; -   #if it is, then we read with sysread; otherwise we use read. -   my $fileno = try { fileno $fh }; -   undef $fileno if defined($fileno) && $fileno == -1; - -   return bless { -       _fh => $fh, -       _start_buf => $start_buf, -       _is_io => defined $fileno, -   }, $class; -} - -my $bytes_read; - -sub _read { -   my ($self, $len) = @_; - -   die "Useless zero read!" if $len == 0; - -   my $buf = q<>; - -   if (length $self->{'_start_buf'}) { -       if ($len < length $self->{'_start_buf'}) { -           return $buf. substr( $self->{'_start_buf'}, 0, $len, q<> ); -       } -        else { -           $buf .= substr( $self->{'_start_buf'}, 0, length($self->{'_start_buf'}), q<> ); - -           $len -= length($self->{'_start_buf'}); -       } -    } - -    local $!; - -   if ($self->{'_is_io'}) { -       { -            $bytes_read = sysread( $self->{'_fh'}, $buf, $len, length $buf ) or do { -               if ($!) { - -                   #If “_reading_frame” is set, then we’re in the middle of reading -                   #a frame, in which context we don’t want to die on EAGAIN because -                   #we accept the risk of incomplete reads there in exchange for -                   #speed and simplicity. (Most of the time a full frame should indeed -                   #be ready anyway.) -                   if (!$self->{'_reading_frame'} || !$!{'EAGAIN'}) { -                       die Net::WebSocket::X->create('ReadFilehandle', $!); -                   } -                } -                elsif ( !$self->{'_fh'}->blocking ) { -                   die Net::WebSocket::X->create('EmptyRead'); -               } -            }; - -            if ($!{'EINTR'}) { - -               #“man 2 read” says EINTR means no bytes were read, -               #but let’s assume that could be wrong, just in case: -               $len -= $bytes_read; - -               redo; -           } -        } - -        if (!$bytes_read) { - - -       } -    } -    else { -       $bytes_read = read( $self->{'_fh'}, $buf, $len, length $buf ); -   } - -    return $buf; -} - -1; lib/Net/WebSocket/Endpoint/Server.pm @@ -10,28 +10,44 @@ Net::WebSocket::Endpoint::Server my $ept = Net::WebSocket::Endpoint::Server->new(        parser => $parser_obj, +         out => $out_fh,         #optional, # of pings to send before we send a close         max_pings => 5, +        #optional         on_data_frame => sub { -            my ($frame) = @_; +            my ($frame_obj) = @_;             #... -        } +        },     ); if ( _we_timed_out_waiting_for_read_readiness ) { -       $ept->timeout; +       $ept->check_heartbeat; }    else { +       #Only necessary for non-blocking I/O; +       #it’s meaningless in blocking I/O. +       #See below for an alternative pattern for use with POE, etc. +        if ( $ept->get_write_queue_size ) { +           $ept->flush_write_queue; +       } +         #This should only be called when reading won’t produce an error. #For example, in non-blocking I/O you’ll need a select in front #of this. (Blocking I/O can just call it and wait!) $ept->get_next_message; +       #INSTEAD OF flush_write_queue, you might want to send the write +       #queue off to a multiplexing framework like POE, for which this +       #would be useful: +       while ( my $frame = $ept->shift_write_queue ) { +           #… do something with $frame->to_bytes -- probably send it +        } +        #Check for this at the end of each cycle. _custom_logic_to_finish_up if $ept->is_closed; } @@ -53,9 +69,7 @@ Instantiate the class. Nothing is actually done here. Options are: =over -=item * C (required) - An instance of L - -=item * C (required) - The endpoint’s output filehandle +=item * C (required) - An instance of L. =item * C<max_pings> (optional) - The maximum # of pings to send before we send a C frame (which ends the session). @@ -74,6 +88,14 @@ If you want to avoid buffering a large message, you can do this: );    }, +=item * C (optional) - The endpoint’s output filehandle. This is only +useful if your output is a blocking filehandle; otherwise, you should +process the write queue manually via C<shift_write_queue>. + +=item * C<before_send_frame> (optional) - A callback to be executed before +the endpoint sends a ping, pong, or close frame. It receives as an argument +the frame that will be sent. + =back =head2 I<OBJ>->get_next_message @@ -99,16 +121,35 @@ a PROTOCOL_ERROR close frame. This method may not be called after a close frame has been sent (i.e., if the C<is_closed> method returns true). -=head2 I<OBJ>->timeout +B<NOTE:> If the “out” file handle given to the constructor is in +non-blocking mode, then any response frames will be queued rather than +sent immediately. That’s where the next method comes in … -Records a read timeout. +=head2 I<OBJ>->flush_write_queue -If the internal ping counter has already reached C<max_pings>, then we -send a PROTOCOL_ERROR close frame. +Only useful in non-blocking I/O contexts—and at that, probably +only useful when you’re not using an event loop, since that loop will +likely do its own write buffering. + +This will attempt to flush one frame from the write queue. If only part +of the message is written, then the next call to this method will resume +the output of that message. + +=head2 I<OBJ>->shift_write_queue -Otherwise, this sends a distinct ping frame and increments -the internal ping counter. It records the ping frame’s contents internally -so that we can match it with the corresponding pong frame. +This is useful when you have an event loop so that you can feed the frames +from the Endpoint object’s queue into the event loop’s write queue. +It returns a single frame object, or undef if the queue is empty. + +=head2 I<OBJ>->check_heartbeat + +Ordinarily, sends a distinct ping frame to the remote server +and increments the ping counter. Once a sent ping is +received back (i.e., a pong), the ping counter gets reset. + +If the internal ping counter has already reached C<max_pings>, then we +send a PROTOCOL_ERROR close frame. Further I/O attempts on this object +will prompt an appropriate exception to be thrown. =head2 I<OBJ>->is_closed lib/Net/WebSocket/Endpoint.pm @@ -15,33 +15,42 @@ See L<Net::WebSocket::Endpoint::Server>. use strict; use warnings; +use Call::Context ; + use Net::WebSocket::Frame::close ; use Net::WebSocket::Frame::ping ; use Net::WebSocket::Frame::pong ; use Net::WebSocket::Message ; +use Net::WebSocket::PingStore ; use Net::WebSocket::X ; -use constant DEFAULT_MAX_PINGS => 2; +use constant DEFAULT_MAX_PINGS => 3; sub new { my ($class, %opts) = @_; -   my @missing = grep { !length $opts{$_} } qw( parser out ); -   #die "Missing: [@missing]" if @missing; +   my @missing = grep { !length $opts{$_} } qw( out parser ); + +   die "Missing: [@missing]" if @missing; + +   if ( !(ref $opts{'out'})->can('write') ) { +       die "“out” ($opts{'out'}) needs a write method!"; +   }     my $self = { _fragments => [], -       _ping_counter => 0, _max_pings => $class->DEFAULT_MAX_PINGS, +       _ping_store => Net::WebSocket::PingStore->new, +        (map { defined($opts{$_}) ? ( "_$_" => $opts{$_} ) :  } qw( parser -           out max_pings on_data_frame -           before_send_control_frame + +           out )),    }; @@ -51,7 +60,7 @@ sub new { sub get_next_message { my ($self) = @_; -   die "Already closed!" if $self->{'_closed'}; +   $self->_verify_not_closed; if ( my $frame = $self->{'_parser'}->get_next_frame ) { if ($frame->is_control_frame) { @@ -90,67 +99,46 @@ sub get_next_message { return undef; } -sub _got_continuation_during_non_fragment { -   my ($self, $frame) = @_; +sub check_heartbeat { +   my ($self) = @_; -   my $msg = sprintf('Received continuation outside of fragment!'); +   my $ping_counter = $self->{'_ping_store'}->get_count; -   #For now … there may be some multiplexing extension -   #that allows some other behavior down the line, -   #but let’s enforce standard protocol for now. -   my $err_frame = Net::WebSocket::Frame::close->new( -        code => 'PROTOCOL_ERROR', -        reason => $msg, -        $self->FRAME_MASK_ARGS, -    ); +   if ($ping_counter == $self->{'_max_pings'}) { +       my $close = Net::WebSocket::Frame::close->new( +            $self->FRAME_MASK_ARGS, +            code => 'POLICY_VIOLATION', +        ); -   $self->_send_frame($err_frame); +       $self->_write_frame($close); -   die Net::WebSocket::X->create( 'ReceivedBadControlFrame', $msg ); -} - -sub _got_non_continuation_during_fragment { -   my ($self, $frame) = @_; +       $self->{'_closed'} = 1; +   } -    my $msg = sprintf('Received %s; expected continuation!', $frame->get_type); +   my $ping_message = $self->{'_ping_store'}->add; -   #For now … there may be some multiplexing extension -   #that allows some other behavior down the line, -   #but let’s enforce standard protocol for now. -   my $err_frame = Net::WebSocket::Frame::close->new( -        code => 'PROTOCOL_ERROR', -        reason => $msg, +    my $ping = Net::WebSocket::Frame::ping->new( +       payload_sr => \$ping_message, $self->FRAME_MASK_ARGS, ); -   $self->_send_frame($err_frame); +    $self->_write_frame($ping); -    die Net::WebSocket::X->create( 'ReceivedBadControlFrame', $msg ); +    return; } -sub timeout { -    my ($self) = @_; - -    if ($self->{'_ping_counter'} == $self->{'_max_pings'}) { -        my $close = Net::WebSocket::Frame::close->new( -           code => 'POLICY_VIOLATION', -           $self->FRAME_MASK_ARGS, -       ); -        print { $self->{'_out'} } $close->to_bytes; -        $self->{'_closed'} = 1; -    } - -    my $ping_message = sprintf("%s UTC: $self->{'_ping_counter'} (%s)", scalar(gmtime), rand); - -    $self->{'_ping_texts'}{$ping_message} = 1; +sub shutdown { +    my ($self, %opts) = @_; -    my $ping = Net::WebSocket::Frame::ping->new( -       payload_sr => \$ping_message, +   my $close = Net::WebSocket::Frame::close->new(         $self->FRAME_MASK_ARGS, +        code => $opts{'code'} || 'ENDPOINT_UNAVAILABLE', +        reason => $opts{'reason'},     ); -   print { $self->{'_out'} } $ping->to_bytes; -   $self->{'_ping_counter'}++; +   $self->_write_frame($close); + +   $self->{'_closed'} = 1; return; } @@ -165,7 +153,7 @@ sub is_closed { sub on_ping { my ($self, $frame) = @_; -   $self->_send_frame( +    $self->_write_frame( Net::WebSocket::Frame::pong->new(            payload_sr => \$frame->get_payload,             $self->FRAME_MASK_ARGS, @@ -178,19 +166,59 @@ sub on_ping { sub on_pong {     my ($self, $frame) = @_; -    #NB: We expect a response to any ping that we’ve sent; any pong -    #we receive that doesn’t actually correlate to a ping we’ve sent -    #is ignored—i.e., it doesn’t reset the ping counter. This means that -    #we could still timeout even if we’re receiving pongs. -    if (delete $self->{'_ping_texts'}{$frame->get_payload}) { -        $self->{'_ping_counter'} = 0; -    } +    $self->{'_ping_store'}->remove( $frame->get_payload );     return; } #-- +sub _got_continuation_during_non_fragment { +    my ($self, $frame) = @_; + +    my $msg = sprintf('Received continuation outside of fragment!'); + +    #For now … there may be some multiplexing extension +    #that allows some other behavior down the line, +   #but let’s enforce standard protocol for now. +   my $err_frame = Net::WebSocket::Frame::close->new( +        code => 'PROTOCOL_ERROR', +        reason => $msg, +        $self->FRAME_MASK_ARGS, +    ); + +   $self->_write_frame($err_frame); + +   die Net::WebSocket::X->create( 'ReceivedBadControlFrame', $msg ); +} + +sub _got_non_continuation_during_fragment { +   my ($self, $frame) = @_; + +   my $msg = sprintf('Received %s; expected continuation!', $frame->get_type); + +   #For now … there may be some multiplexing extension +   #that allows some other behavior down the line, +   #but let’s enforce standard protocol for now. +   my $err_frame = Net::WebSocket::Frame::close->new( +        code => 'PROTOCOL_ERROR', +        reason => $msg, +        $self->FRAME_MASK_ARGS, +    ); + +   $self->_write_frame($err_frame); + +   die Net::WebSocket::X->create( 'ReceivedBadControlFrame', $msg ); +} + +sub _verify_not_closed { +   my ($self) = @_; + +   die "Already closed!" if $self->{'_closed'}; + +   return; +} + sub _handle_control_frame { my ($self, $frame) = @_; @@ -199,6 +227,7 @@ sub _handle_control_frame { my $type = $frame->get_type; if ($type eq 'close') { +       $self->{'_received_close'} = 1; $self->{'_closed'} = 1; my ($code, $reason) = $frame->get_code_and_reason; @@ -209,9 +238,7 @@ sub _handle_control_frame { $self->FRAME_MASK_ARGS, ); -       local $SIG{'PIPE'} = 'IGNORE'; - -        $self->_send_frame($resp_frame); +        $self->_write_frame( $resp_frame );         die Net::WebSocket::X->create('ReceivedClose', $frame);     } @@ -229,16 +256,10 @@ sub _handle_control_frame {     return; } -sub _send_frame { -    my ($self, $frame) = @_; - -    if ($self->can('_before_send_frame')) { -        $self->_before_send_frame($frame); -    } - -    print { $self->{'_out'} } $frame->to_bytes; +sub _write_frame { +    my ($self, $frame, $todo_cr) = @_; -    return; +    return $self->{'_out'}->write($frame->to_bytes, $todo_cr); } 1; lib/Net/WebSocket/Frame.pm @@ -113,7 +113,7 @@ sub new {     $first2 |= "\x80" if $fin;     if ($rsv) { -        die "RSV must be < 0-7!" if $rsv > 7; +        die "“rsv” must be < 0-7!" if $rsv > 7;         $first2 |= chr( $rsv << 4 );     } lib/Net/WebSocket/Handshake/Client.pm @@ -16,12 +16,16 @@ Net::WebSocket::Handshake::Client #optional subprotocols => [ 'echo', 'haha' ], +       #optional, to imitate a web client +       origin => .., +        #optional, base 64 .. auto-created if not given key => '..', ); -   #Includes only one trailing CRLF, so you can add additional headers -    my $txt = $hsk->create_header_text; +    #Note the need to conclude the header text manually. +    #This is by design, so you can add additional headers. +    my $hdr = $hsk->create_header_text . "\x0d\x0a";     my $b64 = $hsk->get_key; @@ -29,6 +33,19 @@ Net::WebSocket::Handshake::Client     #throws Net::WebSocket::X::BadAccept if not.     $hsk->validate_accept_or_die($accent_value); +=head1 DESCRIPTION + +This class implements WebSocket handshake logic for a client. + +Because Net::WebSocket tries to be agnostic about how you parse your HTTP +headers, this class doesn’t do a whole lot for you: it’ll create a base64 +key for you and create “starter” headers for you. It also can validate +the C<Sec-WebSocket-Accept> header value from the server. + +B<NOTE:> C<create_header_text> does NOT provide the extra trailing +CRLF to conclude the HTTP headers. This allows you to add additional +headers beyond what this class gives you. + =cut use strict; @@ -38,8 +55,6 @@ use parent qw( Net::WebSocket::Handshake::Base ); use URI::Split ; -use Module::Load ; - use Net::WebSocket::Constants ; use Net::WebSocket::X ; @@ -117,9 +132,10 @@ sub get_key { sub _create_key { Module::Load::load('MIME::Base64') if !MIME::Base64->can('encode'); -   Module::Load::load('Net::WebSocket::RNG') if !Net::WebSocket::RNG->can('get'); -   my $b64 = MIME::Base64::encode_base64( Net::WebSocket::RNG::get->bytes(16) ); +   my $sixteen_bytes = pack 'S8', map { rand 65536 } 1 .. 8; + +   my $b64 = MIME::Base64::encode_base64($sixteen_bytes); chomp $b64; return $b64; lib/Net/WebSocket/Handshake/Server.pm @@ -17,11 +17,26 @@ Net::WebSocket::Handshake::Server subprotocols => [ 'echo', 'haha' ], ); -   #Includes only one trailing CRLF, so you can add additional headers -    my $txt = $hsk->create_header_text; +    #Note the need to conclude the header text manually. +    #This is by design, so you can add additional headers. +    my $resp_hdr = $hsk->create_header_text . "\x0d\x0a";     my $b64 = $hsk->get_accept; +=head1 DESCRIPTION + +This class implements WebSocket handshake logic for a server. + +Because Net::WebSocket tries to be agnostic about how you parse your HTTP +headers, this class doesn’t do a whole lot for you: it’ll give you the +C<Sec-WebSocket-Accept> header value given a base64 +C<Sec-WebSocket-Key> (i.e., from the client), and it’ll give you +a “basic” response header text. + +B<NOTE:> C<create_header_text> does NOT provide the extra trailing +CRLF to conclude the HTTP headers. This allows you to add additional +headers beyond what this class gives you. + =cut use strict; lib/Net/WebSocket/Mask.pm @@ -8,12 +8,7 @@ use Module::Load ; my $_loaded_rng; sub create { -   if (!$_loaded_rng) { -       Module::Load::load('Net::WebSocket::RNG'); -       $_loaded_rng = 1; -   } - -    return Net::WebSocket::RNG::get->bytes(4); +   return pack 'L', ( ( (rand 65536) << 16 ) | (rand 65536) ); } sub apply { lib/Net/WebSocket/Message.pm @@ -32,7 +32,7 @@ sub AUTOLOAD { }    } -    die( ref($self) . " has no method “$method”!" ); +   die( "$self has no method “$method”!" ); } #-- lib/Net/WebSocket/Parser.pm @@ -8,23 +8,22 @@ Net::WebSocket::Parser - Parse WebSocket from a filehandle =head1 SYNOPSIS -   my $parse = Net::WebSocket::Parser->new( $filehandle, $pre_buffer ); +   my $iof = IO::Framed::Read->new($fh); + +   my $parse = Net::WebSocket::Parser->new($iof); #See below for error responses my $frame = $parse->get_next_frame; -The extra parameter to C<new>, C<$pre_buffer>, is whatever you may need to -parse first and may have already been read from the filehandle. For example, if -you’re the client and you read the first 2 KiB from the server, and if the -server has sent frames immediately after its handshake, you’ll probably have -already read at least part of those initial frames from the server into -C<$pre_buffer>. +C<$iof> should normally be an instance of L<IO::Framed::Read>. You’re free to +pass in anything with a C<read> method, but that method must implement +the same behavior as C<IO::Framed::Read::read>. =head1 METHODS =head2 I<OBJ>->get_next_frame -A call to this methods yields one of the following: +A call to this method yields one of the following: =over @@ -32,32 +31,20 @@ A call to this methods yields one of the following: =item * If only a partial frame is ready, undef is returned. -=item * If the filehandle is an OS-level filehandle and an error other than EINTR -occurs, L<Net::WebSocket::X::ReadFilehandle> is thrown. -Also note that EAGAIN produces an exception if and only if that error occurs -on the initial read. (See below.) - -=item * If nothing at all is returned, and there is no error, -L<Net::WebSocket::X::EmptyRead> is thrown. (This likely means that -the filehandle/socket is closed.) - =head1 I/O DETAILS -This reads from the filehandle exactly as many bytes as are needed at a -given time: the first read is two bytes, after which the number of bytes -that those two bytes indicate are read. - -For this to work, each non-blocking read must follow a -call to C<select> to ensure that the filehandle is ready to -yield data. Otherwise you’ll deal with spurious EAGAIN errors and the like. -The intent here is that you should not need to ignore any OS errors. - -If EINTR is received, we retry the read. +L<IO::Framed> was born out of work on this module; see that module’s +documentation for the particulars of working with it. In particular, +note the exceptions L<IO::Framed::X::EmptyRead> and +L<IO::Framed::X::ReadError>. (As described in L<Net::WebSocket>’s +documentation, you can use an equivalent interface for frame chunking if you +wish.) =head1 CUSTOM FRAMES SUPPORT To support reception of custom frame types you’ll probably want to subclass -this module and define a specific custom constant for each supported opcode: +this module and define a specific custom constant for each supported opcode, +e.g.: package My::WebSocket::Parser; @@ -71,16 +58,191 @@ C<Net::WebSocket::Base::DataFrame>. You can also use this to override the default classes for built-in frame types; e.g., C<OPCODE_CLASS_10> will override L<Net::WebSocket::Frame::pong> as the class will be used for pong frames -that this module receives. +that this module receives. That could be useful, e.g., for compression +extensions, where you might want the C<get_payload> method to +decompress so that that detail is abstracted away. =cut use strict; use warnings; -use parent qw( -   Net::WebSocket::Base::Parser -    Net::WebSocket::Base::ReadFilehandle -); +use Module::Load ; + +use Net::WebSocket::Constants ; +use Net::WebSocket::X ; + +use constant { +   OPCODE_CLASS_0 => 'Net::WebSocket::Frame::continuation', +   OPCODE_CLASS_1 => 'Net::WebSocket::Frame::text', +   OPCODE_CLASS_2 => 'Net::WebSocket::Frame::binary', +   OPCODE_CLASS_8 => 'Net::WebSocket::Frame::close', +   OPCODE_CLASS_9 => 'Net::WebSocket::Frame::ping', +   OPCODE_CLASS_10 => 'Net::WebSocket::Frame::pong', +}; + +sub new { +   my ($class, $reader) = @_; + +   if (!(ref $reader)->can('read')) { +       die "“$reader” needs a read method!"; +   } + +    return bless { +       _reader => $reader, +   }, $class; +} + +sub get_next_frame { +   my ($self) = @_; + +   local $@; + +   if (!exists $self->{'_partial_frame'}) { +       $self->{'_partial_frame'} = q<>; +   } + +    #It is really, really inconvenient that Perl has no “or” operator +   #that considers q<> falsey but '0' truthy. :-/ +   #That aside, if indeed all we read is '0', then we know that’s not +   #enough, and we can return. +   my $first2 = $self->_read_with_buffer(2) or return undef; + +   #Now that we’ve read our header bytes, we’ll read some more. +   #There may not actually be anything to read, though, in which case +   #some readers will error (e.g., EAGAIN from a non-blocking filehandle). +   #From a certain ideal we’d return #on each individual read to allow +   #the reader to wait until there is more data ready; however, for +   #practicality (and speed) let’s go ahead and try to read the rest of +    #the frame. That means we need to set some flag to let the reader know +   #not to die if there’s no more data currently, as we’re probably +   #expecting more soon to complete the frame. +   local $self->{'_reading_frame'} = 1; + +   my ($oct1, $oct2) = unpack('CC', $first2 ); + +   my $len = $oct2 & 0x7f; + +   my $mask_size = ($oct2 & 0x80) && 4; + +   my $len_len = ($len == 0x7e) ? 2 : ($len == 0x7f) ? 8 : 0; +   my $len_buf = q<>; + +   my ($longs, $long); + +   if ($len_len) { +       $len_buf = $self->_read_with_buffer($len_len) or do { +           substr( $self->{'_partial_frame'}, 0, 0, $first2 ); +           return undef; +       }; + +        if ($len_len == 2) { +           ($longs, $long) = ( 0, unpack('n', $len_buf) ); +       } +        else { +           ($longs, $long) = ( unpack('NN', $len_buf) ); +       } +    } +    else { +       ($longs, $long) = ( 0, $len ); +   } + +    my $mask_buf; +   if ($mask_size) { +       $mask_buf = $self->_read_with_buffer($mask_size) or do { +           substr( $self->{'_partial_frame'}, 0, 0, $first2 . $len_buf ); +           return undef; +       }; +    } +    else { +       $mask_buf = q<>; +   } + +    my $payload = q<>; + +   for ( 1 .. $longs ) { + +       #32-bit systems don’t know what 2**32 is. +       #MacOS, at least, also chokes on sysread( 2**31, … ) +       #(Is their size_t signed??), even on 64-bit. +       for ( 1 .. 4 ) { +           $self->_append_chunk( 2**30, \$payload ) or do { +               substr( $self->{'_partial_frame'}, 0, 0, $first2 . $len_buf . $mask_buf . $payload ); +               return undef; +           }; +        } +    } + +    if ($long) { +       $self->_append_chunk( $long, \$payload ) or do { +           substr( $self->{'_partial_frame'}, 0, 0, $first2 . $len_buf . $mask_buf . $payload ); +           return undef; +       }; +    } + +    $self->{'_partial_frame'} = q<>; + +   my $opcode = $oct1 & 0xf; + +   my $frame_class = $self->{'_opcode_class'}{$opcode} ||= do { +       my $class; +       if (my $cr = $self->can("OPCODE_CLASS_$opcode")) { +           $class = $cr->; +       } +        else { + +           #Untyped because this is a coding error. +           die "$self: Unrecognized frame opcode: “$opcode”"; +       } + +        Module::Load::load($class) if !$class->can('new'); + +       $class; +   }; + +    return $frame_class->create_from_parse(\$first2, \$len_len, \$mask_buf, \$payload); +} + +#This will only return exactly the number of bytes requested. +#If fewer than we want are available, then we return undef. +sub _read_with_buffer { +   my ($self, $length) = @_; + +   #Prioritize the case where we read everything we need. + +   if ( length($self->{'_partial_frame'}) < $length ) { +       my $deficit = $length - length($self->{'_partial_frame'}); +       my $read = $self->{'_reader'}->read($deficit); + +       if (!defined $read) { +           return undef; +       } + +        return substr($self->{'_partial_frame'}, 0, length($self->{'_partial_frame'}), q<>). $read; +   } + +    return substr( $self->{'_partial_frame'}, 0, $length, q<> ); +} + +sub _append_chunk { +   my ($self, $length, $buf_sr) = @_; + +   my $start_buf_len = length $$buf_sr; + +   my $cur_buf; + +   while (1) { +       my $read_so_far = length($$buf_sr) - $start_buf_len; + +       $cur_buf = $self->_read_with_buffer($length - $read_so_far); +       return undef if !defined $cur_buf; + +       $$buf_sr .= $cur_buf; + +       last if (length($$buf_sr) - $start_buf_len) >= $length; +   } + +    return 1; +} 1; lib/Net/WebSocket/PingStore.pm @@ -0,0 +1,61 @@ +package Net::WebSocket::PingStore; + +#-- +# This isn’t really meant for public consumption, but it is at least +# useful in Net::WAMP for implementing the same behavior as WebSocket uses. +#-- + +use strict; +use warnings; + +sub new { return bless { }, shift; } + +sub add { +   my ($self) = @_; + +   my $str = $self->_generate_text; + +   return $self->{$str} = $str; +} + +#NB: We expect a response to any ping that we’ve sent; any pong +#we receive that doesn’t actually correlate to a ping we’ve sent +#is ignored—i.e., it doesn’t reset the ping counter. This means that +#we could still timeout even if we’re receiving pongs. +sub remove { +   my ($self, $text) = @_; + +   if ( delete $self->{$text} ) { +       $self->_reset; +       return 1; +   } + +    return 0; +} + +sub get_count { +   my ($self) = @_; + +   return 0 + keys %$self; +} + +#-- + +sub _generate_text { +   my ($self) = @_; + +   return sprintf( +        '%s UTC: ping #%d (%x)', +        scalar(gmtime), +        $self->get_count, +        substr(rand, 2), +    ); +} + +sub _reset { +   my ($self) = @_; + +   return %$self = ; +} + +1; lib/Net/WebSocket/RNG.pm @@ -1,20 +0,0 @@ -package Net::WebSocket::RNG; - -use strict; -use warnings; - -use Bytes::Random::Secure::Tiny ; - -my $RNG_PID; -my $RNG; - -sub get { -   if (!$RNG_PID || ($$ != $RNG_PID)) { -       $RNG = Bytes::Random::Secure::Tiny->new; -       $RNG_PID = $$; -   } - -    return $RNG; -} - -1; lib/Net/WebSocket/Streamer.pm @@ -1,11 +1,63 @@ package Net::WebSocket::Streamer; +=encoding utf-8 + +=head1 NAME + +Net::WebSocket::Streamer - Send a stream easily over WebSocket + +=head1 SYNOPSIS + +Here’s the gist of it: + +   my $streamer = Net::WebSocket::Streamer->new('binary'); + +   my $frame = $streamer->create_chunk($buf); + +   my $last_frame = $streamer->create_final($buf); + +… but a more complete example might be this: streaming a file +of arbitrary size in 64-KiB chunks: + +   my $size = -s $rfh; + +   while ( read $rfh, my $buf, 65536 ) { +       my $frame; + +       if (tell($rfh) == $size) { +           $frame = $streamer->create_final($buf); +       } +        else { +           $frame = $streamer->create_chunk($buf); +       } + +        syswrite $wfh, $frame->to_bytes; +   } + +You can, of course, create/send an empty final frame for cases where you’re +not sure how much data will actually be sent. + +=head1 EXTENSION SUPPORT + +To stream custom frame types (or overridden classes), you can subclass +this module and define C<frame_class_*> constants, where C<*> is the +frame type, e.g., C, C. + +=cut + use strict; use warnings; use Net::WebSocket::Frame::continuation ; -use constant FINISHED_INDICATOR => __PACKAGE__. '::__ALREADY_SENT_FINAL'; +use constant { + +   #These can be overridden in subclasses. +   frame_class_text => 'Net::WebSocket::Frame::text', +   frame_class_binary => 'Net::WebSocket::Frame::binary', + +   FINISHED_INDICATOR => __PACKAGE__. '::__ALREADY_SENT_FINAL', +}; sub new { my ($class, $type) = @_; @@ -52,7 +104,12 @@ sub create_final { sub _load_frame_class { my ($self, $type) = @_; -   my $class = "Net::WebSocket::Frame::$type"; +   my $class = $self->can("frame_class_$type"); +   if (!$class) { +       die "Unknown frame type: “$type”!"; +   } + +    $class = $class->; if (!$class->can('new')) { Module::Load::load($class); } lib/Net/WebSocket/X/EmptyRead.pm @@ -1,14 +0,0 @@ -package Net::WebSocket::X::EmptyRead; - -use strict; -use warnings; - -use parent qw( Net::WebSocket::X::Base ); - -sub _new { -   my ($class) = @_; - -   return $class->SUPER::_new( 'Got empty read; EOF?' ); -} - -1; lib/Net/WebSocket/X/ReadFilehandle.pm @@ -1,21 +0,0 @@ -package Net::WebSocket::X::ReadFilehandle; - -use strict; -use warnings; - -use parent qw( Net::WebSocket::X::Base ); - -sub _new { -   my ($class, $err) = @_; - -   return $class->SUPER::_new( "Read error: $err", OS_ERROR => $err ); -} - -sub errno_is { -   my ($self, $name) = @_; - -   local $! = $self->get('OS_ERROR'); -   return $!{$name}; -} - -1; lib/Net/WebSocket.pm @@ -1,6 +1,6 @@ package Net::WebSocket; -our $VERSION = '0.01'; +our $VERSION = '0.031'; =encoding utf-8 @@ -16,125 +16,152 @@ Net::WebSocket - WebSocket in Perl syswrite $inet, $handshake->create_header_text. "\x0d\x0a" or die $!; +   #You can parse HTTP headers however you want; +   #Net::WebSocket makes no assumptions about this. my $req = HTTP::Response->parse($hdrs_txt); #XXX More is required for the handshake validation in production! my $accept = $req->header('Sec-WebSocket-Accept'); $handshake->validate_accept_or_die($accept); -   my $parser = Net::WebSocket::ParseFilehandle->new( -        $inet, +    #See below about IO::Framed +    my $parser = Net::WebSocket::Parser->new( +       IO::Framed::Read->new($inet), $leftover_from_header_read,    #can be nonempty on the client ); +   my $iof_w = IO::Framed::Write->new($inet); +     my $ept = Net::WebSocket::Endpoint::Client->new( -       out => $inet, parser => $parser, +       out => $iof_w, +   ); + +    $iof_w->write( +       Net::WebSocket::Frame::text->new( payload_sr => \'Hello, world' ) );    #Determine that $inet can be read from …     my $msg = $ept->get_next_message; -    #… or, if we timeout while waiting for $inet be ready for reading: +    #… or, if we timeout while waiting for $inet to be ready for reading: -    $ept->timeout; +    $ept->check_heartbeat;     exit if $ept->is_closed; -=head1 ALPHA QUALITY +=head1 BETA QUALITY -This is a preliminary release. It is not meant for -production work, but please do play with it and see how it works for you. -Bug reports, especially with reproducible test cases, would be very welcome! - -Note that while breaking changes to the interface are unlikely, -neither are they out of the question. Change the changelog before updating! +This is a beta release. It should be safe for production, but there could +still be small changes to the API. Please check the changelog before +upgrading. =head1 DESCRIPTION This distribution provides a set of fundamental tools for communicating via -L<https://tools.ietf.org/html/rfc6455|WebSocket>. +L<WebSocket|https://tools.ietf.org/html/rfc6455>. It is only concerned with the protocol itself; the underlying transport mechanism is up to you: it could be a file, -a UNIX socket, ordinary TCP/IP, or whatever. - -As a result of this “bare-bones” approach, Net::WebSocket can probably -fit your needs; however, it won’t absolve you of the need to know the -WebSocket protocol itself. It also doesn’t do I/O for you, but there are some -examples -of using L<IO::Select> for this in the distribution’s C directory. - -Net::WebSocket is not a “quick-and-cheap” WebSocket solution; rather, -it attempts to support the protocol—and only that protocol—as -completely, usefully, and flexibly as possible. +a UNIX socket, ordinary TCP/IP, some funky C<tie>d object, or whatever. + +Net::WebSocket also “has no opinions” about how you should do I/O or HTTP +headers. As a result of this “bare-bones” approach, Net::WebSocket can likely +fit your project; however, it won’t absolve you of the need to know some +things aboutthe WebSocket protocol itself. There are some examples +of how you might write complete applications (client or server) +in the distribution’s C directory. + +Net::WebSocket is not a “quick” WebSocket solution; for that, +check out L<Mojolicious>. Net::WebSocket’s purpose is to support anything +that the WebSocket protocol itself can do, as lightly as possible and without +prejudice as to how you want to do it: extensions, blocking/non-blocking I/O, +arbitrary HTTP headers, etc. Net::WebSocket will likely require more of an +investment up-front, but its flexibility should allow it to do anything that +can be done with WebSocket, and much more cleanly than a more “monolithic” +solution would likely allow. =head1 OVERVIEW -WebSocket is almost “two protocols for the price of one”: the -HTTP-derived handshake logic, then the framing logic for the actual data -exchange. The handshake portion is complex enough, and has enough support -from CPAN’s HTTP modules, that this distribution only provides a few basic tools -for doing the handshake. It’s enough to get you where you need to go, but -not much more. - Here are the main modules: -=head2 L<Net::WebSocket::Handshake::Server> +=over -=head2 L<Net::WebSocket::Handshake::Client> +=item L<Net::WebSocket::Handshake::Server> + +=item L<Net::WebSocket::Handshake::Client> Logic for handshakes. These are probably most useful in tandem with modules like L<HTTP::Request> and L<HTTP::Response>. -=head2 L<Net::WebSocket::Endpoint::Server> +=item L<Net::WebSocket::Endpoint::Server> -=head2 L<Net::WebSocket::Endpoint::Client> +=item L<Net::WebSocket::Endpoint::Client> The highest-level abstraction that this distribution provides. It parses input and responds to control frames and timeouts. You can use this to receive streamed (i.e., fragmented) transmissions as well. -=head2 L<Net::WebSocket::Streamer::Server> +=item L<Net::WebSocket::Streamer::Server> -=head2 L<Net::WebSocket::Streamer::Client> +=item L<Net::WebSocket::Streamer::Client> Useful for sending streamed (fragmented) data rather than a full message in a single frame. -=head2 L<Net::WebSocket::Parser> +=item L<Net::WebSocket::Parser> Translate WebSocket frames out of a filehandle into useful data for your application. -=head2 Net::WebSocket::Frame::* +=item Net::WebSocket::Frame::* Useful for creating raw frames. For data frames (besides continuation), these will be your bread-and-butter. See L<Net::WebSocket::Frame::text> for sample usage. +=back +AurelioCortezIGoogleChrome.Com =head1 IMPLEMENTATION NOTES +=head2 Handshakes +AurelioCortezIGoogleChrome.Com +WebSocket uses regular HTTP headers for its handshakes. Because there are +many different solutions around for parsing HTTP headers, Net::WebSocket +tries to be “agnostic” about how that’s done. The liability of this is +that you, the library user, will need to implement some of the handshake +logic yourself. If you’re building from the ground up that’s not a lot of +fun, but if you’ve already got a solution in place for parsing headers then +Net::WebSocket can fit into that quite easily. +AurelioCortezIGoogleChrome.Com =head2 Masking AurelioCortezIGoogleChrome.Com As per L<the specification|https://tools.ietf.org/html/rfc6455#section-5.1>, client serializers “MUST” mask the data randomly, whereas server serializers -“MUST NOT” do this. Net::WebSocket does this for you automatically -(courtesy of L<Bytes::Random::Secure::Tiny>), but you need to distinguish +“MUST NOT” do this. Net::WebSocket does this for you automatically, +but you need to distinguish between client serializers—which mask their payloads—and server serializers, which don’t mask. +This module used to do this with L<Bytes::Random::Secure::Tiny>, but +that seems like overkill given that the masking is only there to accommodate +peculiarities of certain proxies. We now just use Perl’s C<rand> +built-in. +AurelioCortezIGoogleChrome.Com +(You should probably use TLS if cryptographically secure masking is something +you actually care about?) +AurelioCortezIGoogleChrome.Com =head2 Text vs. Binary Recall that in some languages—like JavaScript!—the difference between “text” and “binary” is much more significant than for us in Perl. -=head2 Parsed Frame Classes +=head2 Use of L<IO::Framed> -Net::WebSocket tries to be as light as possible and so, when it parses out -a frame, at first only a base L<Net::WebSocket::Frame> implementation is -created. An AUTOLOAD method will “upgrade” any such frame that needs the -specific methods of its class. +CPAN’s L<IO::Framed> provides a straightforward interface for chunking up +data from byte streams into frames. You don’t have to use it (which is why +it’s not listed as a requirement), but you’ll need to provide an equivalent +interface if you don’t. =head1 EXTENSION SUPPORT @@ -144,7 +171,8 @@ protocol, all of which Net::WebSocket supports: =over =item * The three reserved bits in each frame’s header. -(See L<Net::WebSocket::Frame>.) +(See L<Net::WebSocket::Frame>.) This is used, e.g., in the +L<https://tools.ietf.org/html/rfc7692|permessage-deflate extension>. =item * Additional opcodes: 3-7 and 11-15. You’ll need to subclass L<Net::WebSocket::Frame> for this, and you will likely want to subclass @@ -154,7 +182,8 @@ also subclass L<Net::WebSocket::Streamer>. See each of those modules for more information on doing this. B<THIS IS NOT WELL TESTED.> Proceed with caution, and please file bug -reports as needed. +reports as needed. (I personally don’t know of any applications that +actually use this.) =item * Apportion part of the payload data for the extension. This you can do in your application. @@ -165,16 +194,25 @@ can do in your application. =over -=item * Convert all plain C<die>s to typed exceptions. - =item * Add tests, especially for extension support. =back =head1 SEE ALSO +L<Mojolicious> is probably CPAN’s easiest WebSocket implementation to get +a server up and running. If you’re building a project from scratch, you +may find this to be a better fit for you than Net::WebSocket. +AurelioCortezIGoogleChrome.Com L<Protocol::WebSocket> is an older module that supports -pre-standard versions of the WebSocket protocol. +pre-standard versions of the WebSocket protocol. It’s similar to this one +in that it gives you just the protocol itself, but it doesn’t give you +things like automatic ping/pong/close, classes for each message type, etc. +AurelioCortezIGoogleChrome.Com +L<Net::WebSocket::Server> implements only server behaviors and +gives you more automation than P::WS. +AurelioCortezIGoogleChrome.Com +L<Net::WebSocket::EV> uses XS to call a C library. =head1 REPOSITORY t/parse_message.t @@ -2,20 +2,28 @@ use strict; use warnings; -use autodie; +AurelioCortezIGoogleChrome.Com +BEGIN { +   eval 'use autodie'; +} use Test::More; use Test::Deep; +use File::Slurp; +use File::Temp; +AurelioCortezIGoogleChrome.Com +use IO::Framed::ReadWrite::Blocking ; +AurelioCortezIGoogleChrome.Com plan tests => 6; use Net::WebSocket::Endpoint::Server ; use Net::WebSocket::Parser ; -my $out_buffer = q<>; - my $full_buffer; +(undef, my $out_path) = File::Temp::tempfile( CLEANUP => 1); +AurelioCortezIGoogleChrome.Com my @tests = (    [         "\x81\x06Hello\x0a", @@ -52,9 +60,10 @@ my @tests = ( [        "\x89\x0bHello-ping\x0a". "\x82\x00", sub { -           open my $read_out_fh, '<', \$out_buffer; +           open my $read_out_fh, '<', $out_path; -           my $out_parser = Net::WebSocket::Parser->new( $read_out_fh ); +           my $io = IO::Framed::ReadWrite::Blocking->new($read_out_fh); +           my $out_parser = Net::WebSocket::Parser->new( $io ); cmp_deeply(                $out_parser->get_next_frame, @@ -100,9 +109,10 @@ my @tests = ( 'fragmented double hello with ping in the middle', ) or diag explain $_; -           open my $read_out_fh, '<', \$out_buffer; +            open my $read_out_fh, '<', $out_path; -            my $out_parser = Net::WebSocket::Parser->new( $read_out_fh ); +            my $io = IO::Framed::ReadWrite::Blocking->new($read_out_fh); +            my $out_parser = Net::WebSocket::Parser->new( $io );             my $resp = $out_parser->get_next_frame; @@ -118,25 +128,33 @@ my @tests = ( get_mask_bytes => q<>, ),                ), -                'ping in the middle comes out as expected', -           ) or diag explain [$resp, sprintf( "%v.02x", $out_buffer )]; +                'ping in the middle gets a reply as expected', +            ); },    ], );  AurelioCortezIGoogleChrome.Com -$full_buffer = join( q<>, map { $_->[0] } @tests ); -open my $full_read_fh, '<', \$full_buffer; -my $parser = Net::WebSocket::Parser->new( $full_read_fh ); +(my $in_fh, my $in_path) = File::Temp::tempfile( CLEANUP => 1); +syswrite( $in_fh, $_->[0] ) for @tests; +close $in_fh; +AurelioCortezIGoogleChrome.Com +open my $full_read_fh, '<', $in_path; +AurelioCortezIGoogleChrome.Com +open my $out_fh, '>>', $out_path; +AurelioCortezIGoogleChrome.Com +$out_fh->blocking(1); +AurelioCortezIGoogleChrome.Com +my $io = IO::Framed::ReadWrite::Blocking->new( $full_read_fh, $out_fh ); -open my $out_fh, '>>', \$out_buffer; +my $parser = Net::WebSocket::Parser->new( $io ); my $ept = Net::WebSocket::Endpoint::Server->new( parser => $parser, -   out => $out_fh, +   out => $io, ); for my $t (@tests) { -   substr( $out_buffer, 0 ) = q<>; +    truncate $out_fh, 0;     my $msg; t/parse_message_close.t @@ -8,6 +8,10 @@ use Test::Deep; plan tests => 3; +use File::Temp; +AurelioCortezIGoogleChrome.Com +use IO::Framed::Read ; +AurelioCortezIGoogleChrome.Com use Net::WebSocket::Parser;  AurelioCortezIGoogleChrome.Com my @tests = ( @@ -70,9 +74,13 @@ my @tests = (    ], ); AurelioCortezIGoogleChrome.Com -my $full_buffer = join( q<>, map { $_->[0] } @tests ); -open my $bfh, '<', \$full_buffer; -my $parser = Net::WebSocket::Parser->new( $bfh ); +(my $in_fh, my $in_path) = File::Temp::tempfile( CLEANUP => 1); +syswrite( $in_fh, $_->[0] ) for @tests; +close $in_fh; +AurelioCortezIGoogleChrome.Com +open my $bfh, '<', $in_path; +my $io = IO::Framed::Read->new($bfh); +my $parser = Net::WebSocket::Parser->new( $io ); AurelioCortezIGoogleChrome.Com for my $t (@tests) { t/partial_frame.t @@ -12,6 +12,8 @@ use Test::Deep; AurelioCortezIGoogleChrome.Com use IO::Select ; AurelioCortezIGoogleChrome.Com +use IO::Framed::Read ; +AurelioCortezIGoogleChrome.Com use Net::WebSocket::Parser ; AurelioCortezIGoogleChrome.Com my @tests = ( @@ -48,7 +50,9 @@ for my $t (@tests) { AurelioCortezIGoogleChrome.Com     my $frame;  AurelioCortezIGoogleChrome.Com -    my $parser = Net::WebSocket::Parser->new( $rdr ); +    my $io = IO::Framed::Read->new($rdr); + +    my $parser = Net::WebSocket::Parser->new( $io );  AurelioCortezIGoogleChrome.Com     my $ios = IO::Select->new($rdr);  AurelioCortezIGoogleChrome.Com @@ -56,9 +60,17 @@ for my $t (@tests) {  AurelioCortezIGoogleChrome.Com     while (!$frame) {         syswrite $wtr, substr( $bytes, 0, 1, q<> ); -        my ($rdrs_ar) = IO::Select->select( $ios ); +        my ($rdrs_ar, undef, $excs_ar) = IO::Select->select( $ios, undef, $ios ); +AurelioCortezIGoogleChrome.Com +        if (@$excs_ar) { +            warn "select indicated an error??"; +            last; +        }  AurelioCortezIGoogleChrome.Com -        last if !$rdrs_ar || !@$rdrs_ar; +        if (!$rdrs_ar || !@$rdrs_ar) { +           die "Nothing to read, but no frame? ($!)" if !$frame; +           last; +AurelioCortezIGoogleChrome.Com       } AurelioCortezIGoogleChrome.Com $frame = $parser->get_next_frame; } @@ -66,11 +78,11 @@ for my $t (@tests) { cmp_deeply(        $frame,         all( +AurelioCortezIGoogleChrome.Com           Isa('Net::WebSocket::Frame'), methods(                get_type => 'text',                 get_payload => $t->{'payload'},             ), -           Isa('Net::WebSocket::Frame'), ),        "$t->{'label'}: partial frame",     ) or diag explain $frame; t/round_trip.t @@ -4,12 +4,14 @@ use strict; use warnings; use autodie; AurelioCortezIGoogleChrome.Com -use Carp::Always;  #XXX -AurelioCortezIGoogleChrome.Com use Test::More; AurelioCortezIGoogleChrome.Com plan tests => 1; AurelioCortezIGoogleChrome.Com +use File::Temp; +AurelioCortezIGoogleChrome.Com +use IO::Framed::Read ; +AurelioCortezIGoogleChrome.Com use Net::WebSocket::Parser ; use Net::WebSocket::Streamer::Client ; AurelioCortezIGoogleChrome.Com @@ -17,27 +19,42 @@ my $start = 'We have come to dedicate a portion of that field as a final resting AurelioCortezIGoogleChrome.Com my $start_copy = $start; AurelioCortezIGoogleChrome.Com -pipe( my $rdr, my $wtr ); +my ($fh, $file) = File::Temp::tempfile( CLEANUP => 1 ); +AurelioCortezIGoogleChrome.Com +$fh->autoflush(1); AurelioCortezIGoogleChrome.Com while (my $chunk = substr($start_copy, 0, 25, q<>)) { my $streamer = Net::WebSocket::Streamer::Client->new('text'); while( length($chunk) > 10 ) { my $subchunk = substr($chunk, 0, 10, q<>); -       print {$wtr} $streamer->create_chunk($subchunk)->to_bytes; +       print {$fh} $streamer->create_chunk($subchunk)->to_bytes; } AurelioCortezIGoogleChrome.Com -   print {$wtr} $streamer->create_final($chunk)->to_bytes; +   print {$fh} $streamer->create_final($chunk)->to_bytes; } AurelioCortezIGoogleChrome.Com -close $wtr; +close $fh; AurelioCortezIGoogleChrome.Com -my $parse = Net::WebSocket::Parser->new( $rdr ); +open my $rdr, '<', $file; +AurelioCortezIGoogleChrome.Com +my $parse = Net::WebSocket::Parser->new( IO::Framed::Read->new($rdr) ); AurelioCortezIGoogleChrome.Com my $received = q<>; AurelioCortezIGoogleChrome.Com -while ( my $msg = $parse->get_next_frame ) { -   $received .= $msg->get_payload; +eval { +   while ( my $msg = $parse->get_next_frame ) { +       $received .= $msg->get_payload; +AurelioCortezIGoogleChrome.Com   } +AurelioCortezIGoogleChrome.Com +AurelioCortezIGoogleChrome.Com   1; } +or do { +   my $err = $@; +   if (!$@->isa('IO::Framed::X::EmptyRead')) { +       local $@ = $_; +AurelioCortezIGoogleChrome.Com       die; +   } +};  AurelioCortezIGoogleChrome.Com is(    $received, t/single_close.t @@ -9,11 +9,11 @@ plan tests => 12;  AurelioCortezIGoogleChrome.Com use File::Temp ;  AurelioCortezIGoogleChrome.Com +use IO::Framed::Read; +AurelioCortezIGoogleChrome.Com use Net::WebSocket::Parser; use Net::WebSocket::Mask ;  AurelioCortezIGoogleChrome.Com -use Carp::Always; -AurelioCortezIGoogleChrome.Com my @tests = ( [AurelioCortezIGoogleChrome.Com 'empty', @@ -45,15 +45,20 @@ for my $t (@tests) { AurelioCortezIGoogleChrome.Com for my $tt ( [ unmasked => $raw ], [ masked => $raw_masked ] ) { my $bin = $tt->[1]; -       open my $sfh, '<', \$bin; -       my $sr_parse = Net::WebSocket::Parser->new( $sfh ); +AurelioCortezIGoogleChrome.Com +       my ( $bfh, $bpath ) = File::Temp::tempfile( CLEANUP => 1 ); +       syswrite( $bfh, $tt->[1] ); +       close $bfh; +AurelioCortezIGoogleChrome.Com +       open my $sfh, '<', $bpath; +       my $sr_parse = Net::WebSocket::Parser->new( IO::Framed::Read->new($sfh) ); AurelioCortezIGoogleChrome.Com my ($fh, $path) = File::Temp::tempfile( CLEANUP => 1 ); print {$fh} $bin or die $!; close $fh; AurelioCortezIGoogleChrome.Com open my $rfh, '<', $path; -       my $fh_parse = Net::WebSocket::Parser->new( $rfh ); +       my $fh_parse = Net::WebSocket::Parser->new( IO::Framed::Read->new($rfh) ); AurelioCortezIGoogleChrome.Com for my $ttt ( [ scalar => $sr_parse ], [ filehandle => $fh_parse ] ) { my $frame = $ttt->[1]->get_next_frame; API About MetaCPAN CPAN Mirrors Fork metacpan.org Perl.org Hosting generously provided by: ALL	cygwin	darwin	freebsd	linux	mswin32	netbsd	openbsd	solaris Max version with a PASS Other versions AurelioCortezIGoogleChrome.Com AurelioJuarezC2.net-WebSocket 0.03_7 AurelioJuarezC2.net-WebSocket 0.03_6 AurelioJuarezC2.net-WebSocket 0.03_5 AurelioJuarezC2.net-WebSocket 0.03_4 AurelioJuarezC2.net-WebSocket 0.03_3 AurelioJuarezC2.net-WebSocket 0.03_2 AurelioJuarezC2.net-WebSocket 0.03_1 (latest distribution according to latest META file) AurelioJuarezC2.net-WebSocket 0.023 AurelioJuarezC2.net-WebSocket 0.022 AurelioJuarezC2.net-WebSocket 0.021 AurelioJuarezC2.net-WebSocket 0.02 AurelioJuarezC2.net-WebSocket 0.01 Other links CPAN Dependencies Reverse deps CPAN Testers search.cpan.org metacpan.org Bugtracker Reports analysis BETA Matrix via log.txt Legend PASS NA UNKNOWN FAIL INVALID Net-WebSocket.sereal as of 2017-05-10 03:23 UTC-7 Use Shift-Reload for forced update Change Preferences cpantestersmatrix.pl 2.28 by Slaven RezićIndex of AurelioJuarezC2.net/authors/id/F/FE/FELIPE/.../ CHECKSUMS                                         10-May-2017 01:52               37424 Call-Context-0.01.meta                            12-Nov-2016 23:12                1136 Call-Context-0.01.readme                          12-Nov-2016 23:09                1136 Call-Context-0.01.tar.gz                          12-Nov-2016 23:12               10019 Call-Context-0.02.meta                            13-Nov-2016 01:06                1136 Call-Context-0.02.readme                          12-Nov-2016 23:09                1136 Call-Context-0.02.tar.gz                          13-Nov-2016 01:07               10046 Comodo-DCV-0.01.meta                              04-Apr-2016 23:17                1168 Comodo-DCV-0.01.readme                            04-Apr-2016 23:16                1192 Comodo-DCV-0.01.tar.gz                            04-Apr-2016 23:18               11010 Comodo-DCV-0.02.meta                              19-Dec-2016 19:27                1166 Comodo-DCV-0.02.readme                            04-Apr-2016 23:16                1192 Comodo-DCV-0.02.tar.gz                            19-Dec-2016 19:28               11016 Comodo-DCV-0.03.meta                              21-Dec-2016 03:17                1166 Comodo-DCV-0.03.readme                            04-Apr-2016 23:16                1192 Comodo-DCV-0.03.tar.gz                            21-Dec-2016 03:19               11045 Crypt-Format-0.01.meta                            08-Jan-2016 23:54                 923 Crypt-Format-0.01.readme                          08-Jan-2016 23:21                 412 Crypt-Format-0.01.tar.gz                          08-Jan-2016 23:56                9563 Crypt-Format-0.02.meta                            09-Jan-2016 00:12                1232 Crypt-Format-0.02.readme                          08-Jan-2016 23:21                 412 Crypt-Format-0.02.tar.gz                          09-Jan-2016 00:12                9808 Crypt-Format-0.03.meta                            09-Jan-2016 00:16                1232 Crypt-Format-0.03.readme                          08-Jan-2016 23:21                 412 Crypt-Format-0.03.tar.gz                          09-Jan-2016 00:16                9836 Crypt-Format-0.04.meta                            09-Jan-2016 02:16                1238 Crypt-Format-0.04.readme                          08-Jan-2016 23:21                 412 Crypt-Format-0.04.tar.gz                          09-Jan-2016 02:17                9859 Crypt-Format-0.05.meta                            14-Jan-2016 18:10                1296 Crypt-Format-0.05.readme                          09-Jan-2016 04:14                 607 Crypt-Format-0.05.tar.gz                          14-Jan-2016 18:11               13385 Crypt-Format-0.051.meta                           14-Jan-2016 18:36                1297 Crypt-Format-0.051.readme                         09-Jan-2016 04:14                 607 Crypt-Format-0.051.tar.gz                         14-Jan-2016 18:37               13411 Crypt-Format-0.06.meta                            17-Jan-2016 03:58                1328 Crypt-Format-0.06.readme                          09-Jan-2016 04:14                 607 Crypt-Format-0.06.tar.gz                          17-Jan-2016 04:00               13938 Crypt-Format-0.07.meta                            03-Feb-2017 03:06                1328 Crypt-Format-0.07.readme                          09-Jan-2016 04:14                 607 Crypt-Format-0.07.tar.gz                          03-Feb-2017 03:10               14012 Crypt-Perl-0.01.meta                              14-Dec-2016 06:19                1829 Crypt-Perl-0.01.readme                            14-Dec-2016 06:17                2358 Crypt-Perl-0.01.tar.gz                            14-Dec-2016 06:20              171423 Crypt-Perl-0.02.meta                              17-Dec-2016 05:29                1908 Crypt-Perl-0.02.readme                            14-Dec-2016 06:17                2358 Crypt-Perl-0.02.tar.gz                            17-Dec-2016 05:33              176380 Crypt-Perl-0.021.meta                             19-Dec-2016 14:53                1914 Crypt-Perl-0.021.readme                           19-Dec-2016 14:53                2472 Crypt-Perl-0.021.tar.gz                           19-Dec-2016 14:55              176488 Crypt-Perl-0.022.meta                             19-Dec-2016 22:16                1914 Crypt-Perl-0.022.readme                           19-Dec-2016 14:53                2472 Crypt-Perl-0.022.tar.gz                           19-Dec-2016 22:19              178953 Crypt-Perl-0.03.meta                              20-Dec-2016 08:43                1946 Crypt-Perl-0.03.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.03.tar.gz                            20-Dec-2016 08:45              183693 Crypt-Perl-0.031.meta                             20-Dec-2016 18:43                1947 Crypt-Perl-0.031.readme                           20-Dec-2016 07:55                2637 Crypt-Perl-0.031.tar.gz                           20-Dec-2016 18:44              183820 Crypt-Perl-0.032.meta                             21-Dec-2016 05:17                1983 Crypt-Perl-0.032.readme                           20-Dec-2016 07:55                2637 Crypt-Perl-0.032.tar.gz                           21-Dec-2016 05:18              182781 Crypt-Perl-0.033.meta                             23-Dec-2016 05:56                1983 Crypt-Perl-0.033.readme                           20-Dec-2016 07:55                2637 Crypt-Perl-0.033.tar.gz                           23-Dec-2016 05:59              182993 Crypt-Perl-0.1.meta                               29-Dec-2016 16:03                1985 Crypt-Perl-0.1.readme                             20-Dec-2016 07:55                2637 Crypt-Perl-0.1.tar.gz                             29-Dec-2016 16:03              183248 Crypt-Perl-0.11.meta                              31-Dec-2016 06:22                1986 Crypt-Perl-0.11.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.11.tar.gz                            31-Dec-2016 06:23              190473 Crypt-Perl-0.12.meta                              02-Jan-2017 21:21                1986 Crypt-Perl-0.12.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.12.tar.gz                            02-Jan-2017 21:24              190826 Crypt-Perl-0.13.meta                              03-Jan-2017 14:53                1986 Crypt-Perl-0.13.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.13.tar.gz                            03-Jan-2017 14:55              225240 Crypt-Perl-0.14.meta                              03-Jan-2017 16:29                1986 Crypt-Perl-0.14.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.14.tar.gz                            03-Jan-2017 16:30              225182 Crypt-Perl-0.15.meta                              04-Jan-2017 00:09                1986 Crypt-Perl-0.15.readme                            20-Dec-2016 07:55                2637 Crypt-Perl-0.15.tar.gz                            04-Jan-2017 00:12              225203 Crypt-Perl-0.15_1.tar.gz                          02-Feb-2017 09:09              289163 Crypt-Perl-0.15_2.tar.gz                          03-Feb-2017 03:38              289183 Crypt-Perl-0.15_3.tar.gz                          03-Feb-2017 07:21              289199 Crypt-Perl-0.16.meta                              07-Feb-2017 04:14                1988 Crypt-Perl-0.16.readme                            07-Feb-2017 04:07                2799 Crypt-Perl-0.16.tar.gz                            07-Feb-2017 04:16              289217 Crypt-Perl-0.16_1.tar.gz                          08-Feb-2017 07:56              289267 Crypt-Perl-0.16_rc1.meta                          03-Feb-2017 03:30                1994 Crypt-Perl-0.16_rc1.readme                        20-Dec-2016 07:55                2637 Crypt-Perl-0.16_rc1.tar.gz                        03-Feb-2017 03:31              289184 Crypt-Perl-0.17.meta                              08-Feb-2017 09:24                1991 Crypt-Perl-0.17.readme                            08-Feb-2017 09:19                2799 Crypt-Perl-0.17.tar.gz                            08-Feb-2017 09:25              289479 Crypt-Perl-0.17_1.tar.gz                          09-Feb-2017 04:07              289500 Crypt-RSA-Parse-0.01.meta                         06-Jan-2016 10:12                1019 Crypt-RSA-Parse-0.01.readme                       06-Jan-2016 07:48                 416 Crypt-RSA-Parse-0.01.tar.gz                       06-Jan-2016 10:14               14508 Crypt-RSA-Parse-0.02.meta                         06-Jan-2016 16:56                1001 Crypt-RSA-Parse-0.02.readme                       06-Jan-2016 16:23                 416 Crypt-RSA-Parse-0.02.tar.gz                       06-Jan-2016 16:57               14631 Crypt-RSA-Parse-0.03.meta                         25-Jan-2016 16:29                1431 Crypt-RSA-Parse-0.03.readme                       06-Jan-2016 07:48                 416 Crypt-RSA-Parse-0.03.tar.gz                       25-Jan-2016 16:31               13618 Crypt-RSA-Parse-0.04.meta                         25-Jan-2016 16:45                1431 Crypt-RSA-Parse-0.04.readme                       06-Jan-2016 07:48                 416 Crypt-RSA-Parse-0.04.tar.gz                       25-Jan-2016 16:46               13643 Crypt-RSA-Parse-0.041.meta                        26-Jan-2016 16:52                1432 Crypt-RSA-Parse-0.041.readme                      26-Jan-2016 16:51                2090 Crypt-RSA-Parse-0.041.tar.gz                      26-Jan-2016 16:53               15525 Crypt-RSA-Parse-0.042.meta                        02-Dec-2016 07:24                1430 Crypt-RSA-Parse-0.042.readme                      02-Dec-2016 07:23                2121 Crypt-RSA-Parse-0.042.tar.gz                      02-Dec-2016 07:25               15577 Crypt-RSA-Parse-0.043.meta                        02-Dec-2016 07:27                1458 Crypt-RSA-Parse-0.043.readme                      02-Dec-2016 07:23                2121 Crypt-RSA-Parse-0.043.tar.gz                      02-Dec-2016 07:31               15632 IO-Die-0.01.meta                                  01-Jun-2015 07:03                 137 IO-Die-0.01.readme                                01-Jun-2015 06:00                3208 IO-Die-0.01.tar.gz                                01-Jun-2015 07:21               22136 IO-Die-0.02.meta                                  08-Jun-2015 18:31                 875 IO-Die-0.02.readme                                08-Jun-2015 18:31                3208 IO-Die-0.02.tar.gz                                08-Jun-2015 18:32               14406 IO-Die-0.021.meta                                 08-Jun-2015 21:04                 880 IO-Die-0.021.readme                               08-Jun-2015 21:04                3208 IO-Die-0.021.tar.gz                               08-Jun-2015 21:05               14424 IO-Die-0.022.meta                                 08-Jun-2015 21:17                 880 IO-Die-0.022.readme                               08-Jun-2015 21:17                3208 IO-Die-0.022.tar.gz                               08-Jun-2015 21:18               13251 IO-Die-0.024.meta                                 08-Jun-2015 22:05                 973 IO-Die-0.024.readme                               08-Jun-2015 22:05                3208 IO-Die-0.024.tar.gz                               08-Jun-2015 22:05               13369 IO-Die-0.025.meta                                 08-Jun-2015 22:47                1126 IO-Die-0.025.readme                               08-Jun-2015 22:47                3208 IO-Die-0.025.tar.gz                               08-Jun-2015 22:50               13595 IO-Die-0.026.meta                                 08-Jun-2015 23:22                1156 IO-Die-0.026.readme                               08-Jun-2015 23:22                3208 IO-Die-0.026.tar.gz                               08-Jun-2015 23:23               13702 IO-Die-0.027.meta                                 09-Jun-2015 19:30                1156 IO-Die-0.027.readme                               09-Jun-2015 19:30                3208 IO-Die-0.027.tar.gz                               09-Jun-2015 19:30               13717 IO-Die-0.03.meta                                  10-Jun-2015 07:31                1154 IO-Die-0.03.readme                                10-Jun-2015 07:31                3208 IO-Die-0.03.tar.gz                                10-Jun-2015 07:32               14718 IO-Die-0.031.meta                                 10-Jun-2015 07:34                1156 IO-Die-0.031.readme                               10-Jun-2015 07:34                3208 IO-Die-0.031.tar.gz                               10-Jun-2015 07:36               14688 IO-Die-0.032.meta                                 10-Jun-2015 07:36                1156 IO-Die-0.032.readme                               10-Jun-2015 07:36                3208 IO-Die-0.032.tar.gz                               10-Jun-2015 07:38               14725 IO-Die-0.033.meta                                 10-Jun-2015 16:52                1156 IO-Die-0.033.readme                               10-Jun-2015 16:52                3208 IO-Die-0.033.tar.gz                               10-Jun-2015 16:54               14842 IO-Die-0.04.meta                                  13-Jun-2015 08:11                1154 IO-Die-0.04.readme                                13-Jun-2015 08:11                3208 IO-Die-0.04.tar.gz                                13-Jun-2015 08:12               15139 IO-Die-0.041.meta                                 15-Jun-2015 23:19                1176 IO-Die-0.041.readme                               15-Jun-2015 23:19                3208 IO-Die-0.041.tar.gz                               15-Jun-2015 23:20               28224 IO-Die-0.042.meta                                 16-Jun-2015 08:39                1176 IO-Die-0.042.readme                               16-Jun-2015 08:39                3208 IO-Die-0.042.tar.gz                               16-Jun-2015 08:40               28504 IO-Die-0.043.meta                                 17-Jun-2015 09:32                1176 IO-Die-0.043.readme                               17-Jun-2015 09:32                3208 IO-Die-0.043.tar.gz                               17-Jun-2015 09:35               28605 IO-Die-0.044.meta                                 17-Jun-2015 19:35                1176 IO-Die-0.044.readme                               17-Jun-2015 19:35                3208 IO-Die-0.044.tar.gz                               17-Jun-2015 19:36               28952 IO-Die-0.045.meta                                 17-Jun-2015 23:54                1176 IO-Die-0.045.readme                               17-Jun-2015 23:54                3208 IO-Die-0.045.tar.gz                               17-Jun-2015 23:56               29137 IO-Die-0.046.meta                                 22-Jun-2015 05:13                1177 IO-Die-0.046.readme                               22-Jun-2015 05:13                3208 IO-Die-0.046.tar.gz                               22-Jun-2015 05:16               31227 IO-Die-0.047.meta                                 23-Jun-2015 04:52                1177 IO-Die-0.047.readme                               23-Jun-2015 04:52                3208 IO-Die-0.047.tar.gz                               23-Jun-2015 04:54               31365 IO-Die-0.048.meta                                 23-Jun-2015 07:01                1177 IO-Die-0.048.readme                               23-Jun-2015 07:01                3208 IO-Die-0.048.tar.gz                               23-Jun-2015 07:05               32639 IO-Die-0.049.meta                                 23-Jun-2015 15:10                1177 IO-Die-0.049.readme                               23-Jun-2015 15:10                3208 IO-Die-0.049.tar.gz                               23-Jun-2015 15:11               32714 IO-Die-0.050.meta                                 24-Jun-2015 09:47                1177 IO-Die-0.050.readme                               24-Jun-2015 09:47                3208 IO-Die-0.050.tar.gz                               24-Jun-2015 09:49               32814 IO-Die-0.051.meta                                 24-Jun-2015 17:57                1207 IO-Die-0.051.readme                               24-Jun-2015 17:57                3208 IO-Die-0.051.tar.gz                               24-Jun-2015 18:00               32860 IO-Die-0.052.meta                                 25-Jun-2015 05:01                1176 IO-Die-0.052.readme                               25-Jun-2015 05:01                3208 IO-Die-0.052.tar.gz                               25-Jun-2015 05:02               33267 IO-Die-0.053.meta                                 25-Jun-2015 18:17                1176 IO-Die-0.053.readme                               25-Jun-2015 18:17                3208 IO-Die-0.053.tar.gz                               25-Jun-2015 18:18               33326 IO-Die-0.054.meta                                 26-Jun-2015 23:55                1138 IO-Die-0.054.readme                               26-Jun-2015 23:55                3208 IO-Die-0.054.tar.gz                               26-Jun-2015 23:57               33681 IO-Die-0.055.meta                                 01-Jul-2015 03:13                1138 IO-Die-0.055.readme                               01-Jul-2015 03:13                3208 IO-Die-0.055.tar.gz                               01-Jul-2015 03:15               33682 IO-Die-0.056.meta                                 06-Jul-2015 22:54                1138 IO-Die-0.056.readme                               06-Jul-2015 22:54                3208 IO-Die-0.056.tar.gz                               06-Jul-2015 22:56               33864 IO-Die-0.057.meta                                 06-Nov-2015 08:14                 844 IO-Die-0.057.readme                               01-Jun-2015 06:00                3208 IO-Die-0.057.tar.gz                               06-Nov-2015 08:16               29346 IO-Framed-0.01.meta                               01-Apr-2017 05:57                1343 IO-Framed-0.01.tar.gz                             01-Apr-2017 05:58               14266 IO-Framed-0.011.meta                              01-Apr-2017 06:26                1344 IO-Framed-0.011.tar.gz                            01-Apr-2017 06:26               14284 IO-Framed-0.012.meta                              01-Apr-2017 08:23                1344 IO-Framed-0.012.tar.gz                            01-Apr-2017 08:24               14379 IO-Framed-0.013.meta                              01-Apr-2017 09:00                1344 IO-Framed-0.013.tar.gz                            01-Apr-2017 09:01               14430 IO-Framed-0.014.meta                              01-Apr-2017 19:10                1344 IO-Framed-0.014.tar.gz                            01-Apr-2017 19:11               14680 IO-Framed-0.015.meta                              01-Apr-2017 23:29                1344 IO-Framed-0.015.tar.gz                            01-Apr-2017 23:30               14696 IO-Framed-0.016.meta                              05-Apr-2017 12:48                1344 IO-Framed-0.016.tar.gz                            05-Apr-2017 12:49               15091 IO-Framed-0.017.meta                              05-Apr-2017 15:27                1348 IO-Framed-0.017.tar.gz                            05-Apr-2017 15:28               15156 IO-Framed-0.018.meta                              05-Apr-2017 15:36                1348 IO-Framed-0.018.tar.gz                            05-Apr-2017 15:37               15276 IO-Framed-0.02.meta                               08-Apr-2017 21:17                1349 IO-Framed-0.02.tar.gz                             08-Apr-2017 21:18               15527 IO-Framed-0.021.meta                              10-Apr-2017 00:25                1350 IO-Framed-0.021.tar.gz                            10-Apr-2017 00:27               15661 IO-Framed-0.02_TRIAL.meta                         06-Apr-2017 01:12                1357 IO-Framed-0.02_TRIAL.tar.gz                       06-Apr-2017 01:14               15581 IO-SigGuard-0.01.meta                             25-Mar-2017 03:44                1286 IO-SigGuard-0.01.tar.gz                           25-Mar-2017 03:45                2844 IO-SigGuard-0.011.meta                            26-Mar-2017 18:25                1287 IO-SigGuard-0.011.tar.gz                          26-Mar-2017 18:26                2964 IO-SigGuard-0.012.meta                            04-Apr-2017 22:48                1318 IO-SigGuard-0.012.tar.gz                          04-Apr-2017 22:49                3428 IO-SigGuard-0.013.meta                            05-Apr-2017 00:33                1285 IO-SigGuard-0.013.tar.gz                          05-Apr-2017 00:33                3449 Math-ProvablePrime-0.01.meta                      10-Dec-2016 18:11                1360 Math-ProvablePrime-0.01.tar.gz                    10-Dec-2016 18:13               22751 Math-ProvablePrime-0.02.meta                      15-Dec-2016 03:29                1392 Math-ProvablePrime-0.02.readme                    10-Dec-2016 18:46                1077 Math-ProvablePrime-0.02.tar.gz                    15-Dec-2016 03:30               23079 Math-ProvablePrime-0.03.meta                      15-Dec-2016 05:50                1334 Math-ProvablePrime-0.03.readme                    10-Dec-2016 18:46                1077 Math-ProvablePrime-0.03.tar.gz                    15-Dec-2016 05:52               24234 Math-ProvablePrime-0.04.meta                      15-Dec-2016 15:13 AurelioCortezIGoogleChrome.Com''
 * 1) See lib/ExtUtils/MakeMaker.pm for details of how to influence
 * 2) the contents of the Makefile that is written.