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:
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 ++:
The 100 most recent releases ( similar to https://metacpan.org/recent )
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 ++:
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:
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;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence # the contents of the Makefile that is written.
-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])><sprintf '\x%02x', ord $1>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])><sprintf '\x%02x', ord $1>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<A-Z><a-z>; - 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<A-Z><a-z>; - 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<A-Z><a-z>; + die "âUpgradeâ must be âwebsocketâ, not â$upgâ!" if $upg ne 'websocket';
- syswrite( $inet, $frame->to_bytes() ); + my $conn = $req->header('connection'); + $conn =~ tr<A-Z><a-z>; + 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<A-Z><a-z>; + die "âUpgradeâ must be âwebsocketâ, not â$upgâ!" if $upg ne 'websocket'; + + my $conn = $req->header('connection'); + $conn =~ tr<A-Z><a-z>; + 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<parser> (required) - An instance of L<Net::WebSocket::Parser> - -=item * C<out> (required) - The endpointâs output filehandle +=item * C<parser> (required) - An instance of L<Net::WebSocket::Parser>.
=item * C<max_pings> (optional) - The maximum # of pings to send before we send a C<close> frame (which ends the session).
@@ -74,6 +88,14 @@ If you want to avoid buffering a large message, you can do this:
); },
+=item * C<out> (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<text>, C<binary>. + +=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%7CWebSocket>. +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<demo/> 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<demo/> 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%7Cpermessage-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