├── .gitignore ├── Makefile ├── README.md ├── bin └── opm ├── init.sql ├── util ├── fmtMd.js └── opm-pkg-indexer.pl └── web ├── conf ├── config.ini ├── get-limiting.conf ├── mime.types ├── nginx.conf └── root-ca.crt ├── css └── main.css ├── docs └── md │ └── docs.md ├── email.ini.example ├── images ├── cancel.png ├── delete.png ├── favicon.ico └── github.png ├── js └── jquery.min.js ├── lua ├── opmserver.lua ├── opmserver │ ├── conf.lua │ ├── crontab.lua │ ├── db.lua │ ├── email.lua │ ├── github_oauth.lua │ ├── init.lua │ ├── log.lua │ ├── paginator.lua │ ├── templates.lua │ ├── utils.lua │ └── view.lua └── vendor │ ├── pgmoon.lua │ ├── pgmoon │ ├── arrays.lua │ ├── crypto.lua │ ├── init.lua │ ├── json.lua │ └── socket.lua │ └── resty │ ├── cookie.lua │ ├── http.lua │ ├── http_headers.lua │ ├── ini.lua │ ├── limit │ └── req.lua │ ├── shell.lua │ └── signal.lua └── templates ├── 404.tt2 ├── analytics.tt2 ├── docs.tt2 ├── error.tt2 ├── footer.tt2 ├── index.tt2 ├── layout.tt2 ├── login.tt2 ├── nav.tt2 ├── package_info.tt2 ├── package_list.tt2 ├── packages.tt2 ├── search.tt2 ├── success.tt2 ├── uploader.tt2 └── uploads.tt2 /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | *.swp 4 | *.swo 5 | .DS_Store 6 | web/logs/ 7 | web/docs/html/ 8 | ebooks.sh 9 | web/email.ini 10 | node_modules 11 | package-lock.json 12 | 13 | # rpm 14 | buildroot -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pidfile = $(abspath web/logs/nginx.pid) 2 | webpath = $(abspath web/) 3 | openresty = openresty 4 | opm = $(abspath bin/opm) --cwd 5 | opm_pkg_indexer = $(abspath util/opm-pkg-indexer.pl) -i 1 6 | tt2_files := $(sort $(wildcard web/templates/*.tt2)) 7 | templates_lua = web/lua/opmserver/templates.lua 8 | md2html = ./util/fmtMd.js 9 | md_files := $(wildcard web/docs/md/*.md) 10 | html_files := $(patsubst web/docs/md/%.md,web/docs/html/%.html,$(md_files)) 11 | 12 | INSTALL ?= install 13 | CP ?= cp 14 | 15 | VERSION ?= 0.1 16 | RELEASE ?= 1 17 | 18 | .PRECIOUS: $(md_files) 19 | .DELETE_ON_ERRORS: $(templates_lua) 20 | 21 | .PHONY: all 22 | all: $(templates_lua) $(html_files) 23 | 24 | $(templates_lua): $(tt2_files) 25 | mkdir -p web/lua/opmserver/ 26 | lemplate --compile $^ > $@ 27 | 28 | .PHONY: html 29 | html: $(html_files) 30 | 31 | web/docs/html/%.html: web/docs/md/%.md 32 | @mkdir -p web/docs/html 33 | $(md2html) $< > $@ 34 | 35 | .PHONY: test 36 | test: | initdb restart 37 | #./bin/opm build 38 | #-time ./bin/opm upload 39 | rm -rf /tmp/final /tmp/failed /tmp/original *.pid 40 | mkdir -p /tmp/incoming /tmp/final /tmp/failed 41 | cd ../lua-resty-lrucache && $(opm) build 42 | cd ../lua-resty-lrucache && $(opm) upload 43 | PATH=$$PWD/bin:$$PATH time $(opm_pkg_indexer) 44 | $(opm) get openresty/lua-resty-lrucache 45 | cd ../lua-resty-core && $(opm) build 46 | cd ../lua-resty-core && $(opm) upload 47 | PATH=$$PWD/bin:$$PATH time $(opm_pkg_indexer) 48 | $(opm) remove openresty/lua-resty-lrucache 49 | $(opm) get openresty/lua-resty-core 50 | curl -H 'Server: opm.openresyt.org' http://localhost:8080/ 51 | 52 | .PHONY: restart 53 | $(MAKE) stop start 54 | 55 | .PHONY: run 56 | run: all 57 | mkdir -p $(webpath)/logs 58 | cd web && $(openresty) -p $$PWD/ 59 | 60 | .PHONY: reload 61 | reload: all 62 | $(openresty) -p $(webpath)/ -t 63 | test -f $(pidfile) 64 | rm -f $(webpath)/logs/error.log 65 | #rm -f /tmp/openresty/* 66 | #kill -USR1 `cat $(pidfile)` 67 | kill -HUP `cat $(pidfile)` 68 | sleep 0.002 69 | 70 | .PHONY: stop 71 | stop: 72 | test -f $(pidfile) 73 | kill -QUIT `cat $(pidfile)` 74 | 75 | .PHONY: restart 76 | restart: 77 | -$(MAKE) stop 78 | sleep 0.01 79 | $(MAKE) run 80 | 81 | .PHONY: check 82 | check: clean 83 | find . -name "*.lua" | lj-releng -L 84 | 85 | .PHONY: initdb 86 | initdb: $(tsv_files) 87 | psql -Uopm opm -f init.sql 88 | 89 | .PHONY: install 90 | install: 91 | $(MAKE) all 92 | $(INSTALL) -d $(DESTDIR) 93 | $(INSTALL) -d $(DESTDIR)web/ 94 | $(CP) -r bin $(DESTDIR)bin 95 | $(CP) -r util $(DESTDIR)util 96 | $(CP) -r web/conf $(DESTDIR)web/conf 97 | rm -f $(DESTDIR)web/conf/config.ini 98 | $(CP) -r web/css $(DESTDIR)web/css 99 | $(CP) -r web/js $(DESTDIR)web/js 100 | $(CP) -r web/images $(DESTDIR)web/images 101 | $(CP) -r web/docs/ $(DESTDIR)web/docs/ 102 | $(CP) -r web/lua $(DESTDIR)web/lua 103 | 104 | .PHONY: rpm 105 | rpm: 106 | rm -rf buildroot 107 | $(MAKE) install DESTDIR=$$PWD/buildroot/usr/local/opm/ 108 | fpm -f -s dir -t rpm -v "$(VERSION)" --iteration "$(RELEASE)" \ 109 | -n opm-server \ 110 | -C $$PWD/buildroot/ -p ./buildroot \ 111 | --vendor "OpenResty.org" --license Proprietary \ 112 | --description 'opm server' --url 'https://www.openresty.org/' \ 113 | -m 'OpenResty ' \ 114 | --license proprietary -a all \ 115 | usr/local/opm 116 | 117 | .PHONY: clean 118 | clean: 119 | rm -f $(webpath)/lua/opmserver/templates.lua 120 | rm -f $(html_files) 121 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- the following commented commands should be run by a super user of postgres: 2 | -- create user opm with password 'buildecosystem'; 3 | -- create database opm; 4 | -- grant all privileges on database opm to opm; 5 | 6 | drop table if exists users cascade; 7 | 8 | -- for github users 9 | create table users ( 10 | id serial primary key, 11 | login varchar(64) not null unique, 12 | 13 | name varchar(128), 14 | avatar_url varchar(1024), 15 | bio text, 16 | blog varchar(1024), 17 | company varchar(128), 18 | location text, 19 | 20 | followers integer not null, 21 | following integer not null, 22 | 23 | public_email varchar(128), 24 | verified_email varchar(128), 25 | 26 | public_repos integer not null, 27 | 28 | github_created_at timestamp with time zone not null, 29 | github_updated_at timestamp with time zone not null, 30 | 31 | created_at timestamp with time zone not null default now(), 32 | updated_at timestamp with time zone not null default now() 33 | ); 34 | 35 | drop table if exists users_sessions cascade; 36 | 37 | create table users_sessions ( 38 | id serial primary key, 39 | user_id integer not null, 40 | token text not null unique, 41 | ip text not null, 42 | user_agent text, 43 | last_active_at timestamp with time zone default now(), 44 | created_at timestamp with time zone not null default now(), 45 | updated_at timestamp with time zone not null default now() 46 | ); 47 | 48 | drop table if exists orgs cascade; 49 | 50 | -- for github organizations 51 | create table orgs ( 52 | id serial primary key, 53 | login varchar(64) not null unique, 54 | 55 | name varchar(128), 56 | avatar_url varchar(1024), 57 | description text, 58 | blog varchar(1024), 59 | company varchar(128), 60 | location text, 61 | 62 | public_email varchar(128), 63 | public_repos integer not null, 64 | 65 | github_created_at timestamp with time zone not null, 66 | github_updated_at timestamp with time zone not null, 67 | 68 | created_at timestamp with time zone not null default now(), 69 | updated_at timestamp with time zone not null default now() 70 | ); 71 | 72 | drop table if exists org_ownership cascade; 73 | 74 | -- for github organization ownership among github users 75 | create table org_ownership ( 76 | id serial primary key, 77 | user_id integer references users(id) not null, 78 | org_id integer references orgs(id) not null 79 | ); 80 | 81 | drop table if exists access_tokens cascade; 82 | 83 | -- for github personal access tokens (we do not store too permissive tokens) 84 | create table access_tokens ( 85 | id serial primary key, 86 | user_id integer references users(id) not null, 87 | token_hash text not null, 88 | scopes text, 89 | created_at timestamp with time zone not null default now(), 90 | updated_at timestamp with time zone not null default now() 91 | ); 92 | 93 | drop table if exists uploads cascade; 94 | 95 | -- for user module uploads 96 | create table uploads ( 97 | id serial primary key, 98 | uploader integer references users(id) not null, 99 | org_account integer references orgs(id), -- only take a value when the 100 | -- account != uploader 101 | orig_checksum uuid not null, -- MD5 checksum for the original pkg 102 | final_checksum uuid, -- MD5 checksum for the final pkg 103 | size integer not null, 104 | package_name varchar(256) not null, 105 | abstract text, 106 | doc text, 107 | 108 | version_v integer[] not null, 109 | version_s varchar(128) not null, 110 | 111 | authors text[], 112 | licenses text[], 113 | is_original boolean, 114 | repo_link varchar(1024), 115 | 116 | dep_packages text[], 117 | dep_operators varchar(2)[], 118 | dep_versions text[], 119 | 120 | client_addr inet not null, 121 | failed boolean not null default FALSE, 122 | indexed boolean not null default FALSE, 123 | is_deleted boolean, 124 | ts_idx tsvector, 125 | 126 | created_at timestamp with time zone not null default now(), 127 | updated_at timestamp with time zone not null default now(), 128 | deleted_at_time integer 129 | ); 130 | 131 | drop function if exists uploads_trigger() cascade; 132 | 133 | create function uploads_trigger() returns trigger as $$ 134 | begin 135 | new.ts_idx := 136 | setweight(to_tsvector('pg_catalog.english', coalesce(new.package_name,'')), 'A') 137 | || setweight(to_tsvector('pg_catalog.english', coalesce(new.abstract,'')), 'B') 138 | || setweight(to_tsvector('pg_catalog.english', coalesce(new.doc,'')), 'D'); 139 | return new; 140 | end 141 | $$ language plpgsql; 142 | 143 | create trigger tsvectorupdate before insert or update 144 | on uploads for each row execute procedure uploads_trigger(); 145 | 146 | create index ts_idx on uploads using gin(ts_idx); 147 | 148 | update uploads set package_name = package_name; 149 | 150 | drop function if exists first_agg(anyelement, anyelement) cascade; 151 | 152 | create or replace function first_agg(anyelement, anyelement) 153 | returns anyelement language sql immutable strict as $$ 154 | select $1; 155 | $$; 156 | 157 | create aggregate first ( 158 | sfunc = first_agg, 159 | basetype = anyelement, 160 | stype = anyelement 161 | ); 162 | 163 | drop function if exists last_agg(anyelement, anyelement) cascade; 164 | 165 | create or replace function last_agg(anyelement, anyelement) 166 | returns anyelement language sql immutable strict as $$ 167 | select $1; 168 | $$; 169 | 170 | create aggregate last ( 171 | sfunc = last_agg, 172 | basetype = anyelement, 173 | stype = anyelement 174 | ); 175 | 176 | -- TODO create more indexes to speed up queries in the opmserver web app. 177 | -------------------------------------------------------------------------------- /util/fmtMd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var marked = require('marked'); 5 | var renderer = new marked.Renderer(); 6 | var matchHtmlRegExp = /["'&<>]/; 7 | 8 | function escapeHtml(string) { 9 | var str = '' + string; 10 | var match = matchHtmlRegExp.exec(str); 11 | 12 | if (!match) { 13 | return str; 14 | } 15 | 16 | var escape; 17 | var html = ''; 18 | var index = 0; 19 | var lastIndex = 0; 20 | 21 | for (index = match.index; index < str.length; index++) { 22 | switch (str.charCodeAt(index)) { 23 | case 34: // " 24 | escape = '"'; 25 | break; 26 | case 38: // & 27 | escape = '&'; 28 | break; 29 | case 39: // ' 30 | escape = '''; 31 | break; 32 | case 60: // < 33 | escape = '<'; 34 | break; 35 | case 62: // > 36 | escape = '>'; 37 | break; 38 | default: 39 | continue; 40 | } 41 | 42 | if (lastIndex !== index) { 43 | html += str.substring(lastIndex, index); 44 | } 45 | 46 | lastIndex = index + 1; 47 | html += escape; 48 | } 49 | 50 | return lastIndex !== index ? 51 | html + str.substring(lastIndex, index) : 52 | html; 53 | } 54 | 55 | marked.setOptions({ 56 | gfm: true, 57 | }); 58 | 59 | // create internal links for ()[#] 60 | renderer.link = function (href, title, text) { 61 | title = title || text; 62 | if (href === '#') { 63 | var slugger = new marked.Slugger(); 64 | href = '#' + slugger.slug(text); 65 | } 66 | title = escapeHtml(title); 67 | return '' + text + ''; 68 | } 69 | 70 | // add link icon for each heading, just like github 71 | renderer.heading = function (text, level, raw, slugger) { 72 | var anchorId = renderer.options.headerPrefix + slugger.slug(raw); 73 | return '' 74 | + '' 75 | + '' 76 | + '' + text 77 | + '\n'; 78 | }; 79 | 80 | var args = process.argv.slice(2); 81 | if (args.length === 0) { 82 | console.log('please provide input file'); 83 | process.exit(0); 84 | } 85 | 86 | var infile = args[0]; 87 | fs.open(infile, 'r', function (err, fd) { 88 | if (err) { 89 | if (err.code === 'ENOENT') { 90 | console.error(infile + ' does not exist'); 91 | return; 92 | } 93 | 94 | throw err; 95 | } 96 | 97 | fs.readFile(infile, 'utf8', function (err, data) { 98 | if (err) throw err; 99 | console.log(marked(data, { 100 | renderer: renderer 101 | })); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /util/opm-pkg-indexer.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # Copyright (C) Yichun Zhang (agentzh) 4 | 5 | use v5.10.1; 6 | use strict; 7 | use warnings; 8 | 9 | use sigtrap qw(die INT QUIT TERM); 10 | use Time::HiRes qw( sleep ); 11 | use URI (); 12 | use LWP::UserAgent (); 13 | use JSON::XS (); 14 | #use Data::Dumper qw( Dumper ); 15 | use File::Spec (); 16 | use File::Copy qw( copy ); 17 | use File::Path qw( make_path ); 18 | use Digest::MD5 (); 19 | use Getopt::Std qw( getopts ); 20 | use Cwd qw( cwd ); 21 | 22 | sub http_req ($$$); 23 | sub main (); 24 | sub process_cycle (); 25 | sub gen_backup_file_prefix (); 26 | sub shell (@); 27 | sub read_ini ($); 28 | sub err_log; 29 | sub shell_quote ($); 30 | 31 | my $json_xs = JSON::XS->new; 32 | 33 | my $version = '0.0.7'; 34 | 35 | my %opts; 36 | getopts("di:", \%opts) or die; 37 | 38 | my $as_daemon = $opts{d}; 39 | my $iterations = $opts{i} || 0; # 0 means infinite 40 | 41 | my $api_server_host = shift || "127.0.0.1"; 42 | my $api_server_port = shift || 8080; 43 | my $failed_dir = shift || "/opm/failed"; 44 | my $original_dir = shift || "/opm/original"; 45 | 46 | my $SpecialDepPat = qr/^(?:openresty|luajit|ngx_(?:http_)?lua|nginx)$/; 47 | 48 | my $name = "opm-pkg-indexer"; 49 | my $pid_file = File::Spec->rel2abs("$name.pid"); 50 | 51 | $ENV{LC_ALL} = 'C'; 52 | 53 | if (!-d $failed_dir) { 54 | make_path $failed_dir; 55 | } 56 | 57 | my $uri_prefix = "http://$api_server_host:$api_server_port"; 58 | 59 | my $ua = LWP::UserAgent->new; 60 | $ua->agent("opm pkg indexer" . $version); 61 | 62 | my $req = HTTP::Request->new(); 63 | $req->header(Host => "opm.openresty.org"); 64 | 65 | my $MAX_SLEEP_TIME = 1; # sec 66 | my $MAX_HTTP_TRIES = 3; 67 | my $MAX_DEPS = 50; 68 | #my $MAX_DEPS = 0; 69 | 70 | if (-f $pid_file) { 71 | open my $in, $pid_file 72 | or die "cannot open $pid_file for reading: $!\n"; 73 | my $pid = <$in>; 74 | close $in; 75 | 76 | chomp $pid; 77 | 78 | if (!$pid) { 79 | unlink $pid_file or die "cannot rm $pid_file: $!\n"; 80 | 81 | } else { 82 | my $file = $pid_file; 83 | undef $pid_file; 84 | die "Found pid file $file. ", 85 | "Another process $pid may still be running.\n"; 86 | } 87 | } 88 | 89 | if ($opts{d}) { 90 | my $log_file = "$name.log"; 91 | 92 | require Proc::Daemon; 93 | 94 | my $daemon = Proc::Daemon->new( 95 | work_dir => cwd, 96 | child_STDOUT => "+>>$log_file", 97 | child_STDERR => "+>>$log_file", 98 | pid_file => $pid_file, 99 | ); 100 | 101 | my $pid = $daemon->Init; 102 | if ($pid == 0) { 103 | # in the forked daemon 104 | 105 | } else { 106 | # in parent 107 | #err_log "write pid file $pid_file: $pid"; 108 | #write_pid_file($pid); 109 | exit; 110 | } 111 | 112 | } else { 113 | write_pid_file($$); 114 | } 115 | 116 | sub cleanup { 117 | if (defined $pid_file && -f $pid_file) { 118 | my $in; 119 | if (open $in, $pid_file) { 120 | my $pid = <$in>; 121 | if ($pid eq $$) { 122 | unlink $pid_file; 123 | } 124 | close $in; 125 | } 126 | } 127 | } 128 | 129 | END { 130 | cleanup(); 131 | exit; 132 | } 133 | 134 | main(); 135 | 136 | unlink $pid_file or die "cannot remove $pid_file: $!\n";; 137 | 138 | sub main () { 139 | my $sleep_time = 0.001; 140 | #err_log "iterations: $iterations"; 141 | for (my $i = 1; $iterations <= 0 || $i <= $iterations; $i++) { 142 | my $ok; 143 | eval { 144 | $ok = process_cycle(); 145 | }; 146 | if ($@) { 147 | err_log "failed to process: $@"; 148 | } 149 | 150 | if (!$ok) { 151 | #err_log $sleep_time; 152 | sleep $sleep_time; 153 | 154 | if ($sleep_time < $MAX_SLEEP_TIME) { 155 | $sleep_time *= 2; 156 | } 157 | 158 | next; 159 | } 160 | 161 | # ok 162 | $sleep_time = 0.001; 163 | } 164 | } 165 | 166 | sub process_cycle () { 167 | my ($data, $err) = http_req("/api/pkg/incoming", undef, undef); 168 | #warn Dumper($data); 169 | 170 | if ($err) { 171 | return; 172 | } 173 | 174 | my $incoming_dir = $data->{incoming_dir} or die; 175 | my $final_dir = $data->{final_dir} or die; 176 | 177 | my $uploads = $data->{uploads}; 178 | my $errstr; 179 | 180 | for my $upload (@$uploads) { 181 | #warn Dumper($upload); 182 | my $id = $upload->{id} or die "id not defined"; 183 | my $name = $upload->{name} or die "name not defined"; 184 | my $ver = $upload->{version_s} or die "version_s not defined"; 185 | my $uploader = $upload->{uploader} or die "uploader not defined"; 186 | my $org_account = $upload->{org_account}; 187 | my $orig_checksum = $upload->{orig_checksum} 188 | or die "orig_checksum not defined"; 189 | $orig_checksum =~ s/-//g; 190 | 191 | my $account; 192 | if ($org_account) { 193 | $account = $org_account; 194 | 195 | } else { 196 | $account = $uploader; 197 | } 198 | 199 | my $fname = "$name-$ver.tar.gz"; 200 | 201 | #warn $fname; 202 | my $path = File::Spec->catfile($incoming_dir, $account, $fname); 203 | if (!-f $path) { 204 | $errstr = "file $path does not exist"; 205 | err_log $errstr; 206 | goto FAIL_UPLOAD; 207 | } 208 | 209 | my $md5sum; 210 | { 211 | my $in; 212 | if (!open $in, $path) { 213 | $errstr = "cannot open $path for reading: $!"; 214 | err_log $errstr; 215 | goto FAIL_UPLOAD; 216 | } 217 | 218 | my $ctx = Digest::MD5->new; 219 | $ctx->addfile($in); 220 | $md5sum = $ctx->hexdigest; 221 | close $in; 222 | } 223 | 224 | if ($md5sum ne $orig_checksum) { 225 | $errstr = "MD5 checksum for the original package mismatch: " 226 | . "$md5sum vs $orig_checksum\n"; 227 | err_log $errstr; 228 | goto FAIL_UPLOAD; 229 | } 230 | 231 | #err_log "file $path found"; 232 | 233 | my $cwd = File::Spec->catdir($incoming_dir, $account); 234 | if (!chdir $cwd) { 235 | $errstr = "failed to chdir to $cwd: $!"; 236 | err_log $errstr; 237 | goto FAIL_UPLOAD; 238 | } 239 | 240 | my $dist_basename = "$name-$ver"; 241 | my $dist_dir = File::Spec->rel2abs( 242 | File::Spec->catdir($cwd, $dist_basename)); 243 | 244 | if (-d $dist_dir) { 245 | shell "rm", "-rf", $dist_dir; 246 | } 247 | 248 | if (!shell "tar", "-xzf", $path) { 249 | $errstr = "failed to unpack $fname"; 250 | err_log $errstr; 251 | goto FAIL_UPLOAD; 252 | } 253 | 254 | my $dir = $dist_basename; 255 | if (!-d $dir) { 256 | $errstr = "directory $dir not found after unpacking $fname"; 257 | err_log $errstr; 258 | goto FAIL_UPLOAD; 259 | } 260 | 261 | if (!chdir $dir) { 262 | $errstr = "failed to chdir to $dir: $!"; 263 | err_log $errstr; 264 | goto FAIL_UPLOAD; 265 | } 266 | 267 | my $out = `ulimit -t 10 -v 204800 && opm server-build 2>&1`; 268 | if ($? != 0) { 269 | $errstr = "failed to run \"opm server-build\":\n$out"; 270 | err_log $errstr; 271 | goto FAIL_UPLOAD; 272 | } 273 | 274 | my $final_file = "$name-$ver.opm.tar.gz"; 275 | if (!-f $final_file) { 276 | $errstr = "failed to find $final_file from \"opm server-build\""; 277 | err_log $errstr; 278 | goto FAIL_UPLOAD; 279 | } 280 | 281 | my $final_md5; 282 | { 283 | my $in; 284 | if (!open $in, $final_file) { 285 | $errstr = "cannot open $final_file for reading: $!\n"; 286 | err_log $errstr; 287 | goto FAIL_UPLOAD; 288 | } 289 | 290 | my $ctx = Digest::MD5->new; 291 | $ctx->addfile($in); 292 | #$ctx->add("foo"); 293 | $final_md5 = $ctx->hexdigest; 294 | close $in; 295 | } 296 | 297 | { 298 | my $final_subdir = File::Spec->catdir($final_dir, $account); 299 | 300 | if (!-d $final_subdir) { 301 | eval { 302 | make_path $final_subdir; 303 | }; 304 | if ($@) { 305 | # failed 306 | $errstr = $@; 307 | err_log $errstr; 308 | goto FAIL_UPLOAD; 309 | } 310 | } 311 | 312 | my $dstfile = File::Spec->catfile($final_subdir, $final_file); 313 | 314 | if (!copy($final_file, $dstfile)) { 315 | $errstr = "failed to copy $path to $dstfile: $!"; 316 | err_log $errstr; 317 | goto FAIL_UPLOAD; 318 | } 319 | } 320 | 321 | my $inifile = "dist.ini"; 322 | my ($user_meta, $err) = read_ini($inifile); 323 | if (!$user_meta) { 324 | $errstr = "failed to load $inifile: $errstr"; 325 | err_log $errstr; 326 | goto FAIL_UPLOAD; 327 | } 328 | 329 | my $default_sec = $user_meta->{default}; 330 | 331 | my $meta_name = $default_sec->{name}; 332 | if (!$meta_name) { 333 | $errstr = "$inifile: no name found"; 334 | err_log $errstr; 335 | goto FAIL_UPLOAD; 336 | } 337 | 338 | if ($meta_name ne $name) { 339 | $errstr = "$inifile: name \"$meta_name\" does not match \"$name\""; 340 | err_log $errstr; 341 | goto FAIL_UPLOAD; 342 | } 343 | 344 | my $meta_account = $default_sec->{account}; 345 | if (!$meta_account) { 346 | $errstr = "$inifile: no account found"; 347 | err_log $errstr; 348 | goto FAIL_UPLOAD; 349 | } 350 | 351 | if ($meta_account ne $account) { 352 | $errstr = "$inifile: account \"$meta_account\" does not match " 353 | . "\"$account\""; 354 | err_log $errstr; 355 | goto FAIL_UPLOAD; 356 | } 357 | 358 | my $authors = $default_sec->{author}; 359 | if (!$authors) { 360 | $errstr = "$inifile: no authors found"; 361 | err_log $errstr; 362 | goto FAIL_UPLOAD; 363 | } 364 | 365 | $authors = [split /\s*,\s*/, $authors]; 366 | 367 | my $repo_link = $default_sec->{repo_link}; 368 | if (!$repo_link) { 369 | $errstr = "$inifile: no repo_link found"; 370 | err_log $errstr; 371 | goto FAIL_UPLOAD; 372 | } 373 | 374 | my $is_orig = $default_sec->{is_original}; 375 | if (!$is_orig) { 376 | $errstr = "$inifile: no repo_link found"; 377 | err_log $errstr; 378 | goto FAIL_UPLOAD; 379 | } 380 | 381 | if ($is_orig eq 'yes') { 382 | $is_orig = 1; 383 | 384 | } elsif ($is_orig eq 'no') { 385 | undef $is_orig; 386 | 387 | } else { 388 | $errstr = "$inifile: bad is_original value: $is_orig"; 389 | err_log $errstr; 390 | goto FAIL_UPLOAD; 391 | } 392 | 393 | my $abstract = $default_sec->{abstract}; 394 | if (!$abstract) { 395 | $errstr = "$inifile: no abstract found"; 396 | err_log $errstr; 397 | goto FAIL_UPLOAD; 398 | } 399 | 400 | my $license = $default_sec->{license}; 401 | if (!$license) { 402 | $errstr = "$inifile: no license found"; 403 | err_log $errstr; 404 | goto FAIL_UPLOAD; 405 | } 406 | 407 | my $licenses = [split /\s*,\s*/, $license]; 408 | 409 | my $requires = $default_sec->{requires}; 410 | my (@dep_pkgs, @dep_ops, @dep_vers); 411 | if ($requires) { 412 | my ($deps, $err) = parse_deps($requires, $inifile); 413 | if ($err) { 414 | $errstr = "$inifile: requires: $err"; 415 | err_log $errstr; 416 | goto FAIL_UPLOAD; 417 | } 418 | 419 | my $ndeps = @$deps; 420 | if ($ndeps > $MAX_DEPS) { 421 | $errstr = "$inifile: too many dependencies: $ndeps"; 422 | err_log $errstr; 423 | goto FAIL_UPLOAD; 424 | } 425 | 426 | for my $dep (@$deps) { 427 | my ($account, $name, $op, $ver) = @$dep; 428 | 429 | my ($op_arg, $ver_arg); 430 | 431 | if ($op && $ver) { 432 | if ($op eq '>=') { 433 | $op_arg = "ge"; 434 | 435 | } elsif ($op eq '=') { 436 | $op_arg = "eq"; 437 | 438 | } elsif ($op eq '>') { 439 | $op_arg = "gt"; 440 | 441 | } else { 442 | $errstr = "bad dependency operator: $op"; 443 | err_log $errstr; 444 | goto FAIL_UPLOAD; 445 | } 446 | 447 | $ver_arg = $ver; 448 | 449 | } else { 450 | $op_arg = ""; 451 | $ver_arg = ""; 452 | } 453 | 454 | die unless !defined $account || $account =~ /^[-\w]+$/; 455 | die unless $name =~ /^[-\w]+$/; 456 | 457 | if ($account) { 458 | my $uri = "/api/pkg/exists?account=$account\&name=$name\&op=$op_arg&version=$ver_arg"; 459 | my ($res, $err) = http_req($uri, undef, { 404 => 1 }); 460 | 461 | if (!$res) { 462 | my $spec; 463 | if (!defined $op || !defined $ver) { 464 | $spec = ''; 465 | } else { 466 | $spec = " $op $ver"; 467 | } 468 | 469 | $errstr = "package dependency check failed on \"$name$spec\": $err"; 470 | err_log $errstr; 471 | goto FAIL_UPLOAD; 472 | } 473 | 474 | #warn $res->{found_version}; 475 | } 476 | 477 | push @dep_pkgs, $account ? "$account/$name" : $name; 478 | push @dep_ops, $op || undef; 479 | push @dep_vers, $ver || undef; 480 | } 481 | } 482 | 483 | my $podfile = "$name-$ver.opm/pod/$name-$ver/$name-$ver.pod"; 484 | my $quoted_podfile = shell_quote $podfile; 485 | my $htmlfile = "/opm/tmp/$final_md5.html"; 486 | my $quoted_htmlfile = shell_quote $htmlfile; 487 | 488 | # warn "pod2html --noindex $quoted_podfile > $quoted_htmlfile"; 489 | if (!shell "pod2html --noindex $quoted_podfile > $quoted_htmlfile") { 490 | $errstr = "failed to run pod2html for $quoted_podfile"; 491 | err_log $errstr; 492 | goto FAIL_UPLOAD; 493 | } 494 | 495 | $htmlfile = File::Spec->rel2abs($quoted_htmlfile); 496 | 497 | $dir = "../.."; 498 | chdir $dir or err_log "cannot chdir $dir: $!"; 499 | 500 | if (-d $dist_dir) { 501 | shell "rm", "-rf", $dist_dir; 502 | } 503 | 504 | { 505 | # back up the original user uploaded file into original_dir. 506 | 507 | my $orig_subdir = File::Spec->catdir($original_dir, $account); 508 | my $failed; 509 | 510 | if (!-d $orig_subdir) { 511 | eval { 512 | make_path $orig_subdir; 513 | }; 514 | if ($@) { 515 | # failed 516 | $failed = 1; 517 | err_log $@; 518 | } 519 | } 520 | 521 | if (!$failed) { 522 | my $dstfile = File::Spec->catfile($orig_subdir, $fname); 523 | 524 | if (!copy($path, $dstfile)) { 525 | err_log "failed to copy $path to $dstfile: $!"; 526 | } 527 | } 528 | } 529 | 530 | my $meta = { 531 | id => $id, 532 | authors => $authors, 533 | abstract => $abstract, 534 | licenses => $licenses, 535 | is_original => $is_orig, 536 | repo_link => $repo_link, 537 | final_checksum => $final_md5, 538 | dep_packages => \@dep_pkgs, 539 | dep_operators => \@dep_ops, 540 | dep_versions => \@dep_vers, 541 | file => $path, 542 | doc_file => $htmlfile, 543 | }; 544 | 545 | { 546 | my $uri = "/api/pkg/processed"; 547 | my ($res, $err) = http_req($uri, $json_xs->encode($meta), undef); 548 | if (!defined $res) { 549 | goto FAIL_UPLOAD; 550 | } 551 | } 552 | 553 | next; 554 | 555 | FAIL_UPLOAD: 556 | 557 | if (-f $path) { 558 | my $failed_subdir = File::Spec->catdir($failed_dir, $account); 559 | 560 | if (!-d $failed_subdir) { 561 | eval { 562 | make_path $failed_subdir; 563 | }; 564 | if ($@) { 565 | # failed 566 | err_log $@; 567 | } 568 | } 569 | 570 | my $prefix = gen_backup_file_prefix(); 571 | my $dstfile = File::Spec->catfile($failed_subdir, "$prefix-$fname"); 572 | 573 | copy($path, $dstfile) 574 | or err_log "failed to copy $path to $dstfile: $!"; 575 | } 576 | 577 | if (-d $dist_dir) { 578 | shell "rm", "-rf", $dist_dir; 579 | } 580 | 581 | { 582 | my $uri = "/api/pkg/processed"; 583 | my $meta = { 584 | id => $id, 585 | failed => 1, 586 | reason => $errstr, 587 | file => $path, 588 | }; 589 | 590 | my ($res, $err) = http_req($uri, $json_xs->encode($meta), undef); 591 | } 592 | } 593 | } 594 | 595 | sub gen_backup_file_prefix () { 596 | my ($sec, $min, $hour, $mday, $mon, $year) = localtime(); 597 | return sprintf("%04d-%02d-%02d-%02d-%02d-%02d", 598 | $year + 1900, $mon + 1, $mday, 599 | $hour, $min, $sec); 600 | } 601 | 602 | sub gen_timestamp () { 603 | my ($sec, $min, $hour, $mday, $mon, $year) = localtime(); 604 | return sprintf("%04d-%02d-%02d %02d:%02d:%02d", 605 | $year + 1900, $mon + 1, $mday, 606 | $hour, $min, $sec); 607 | } 608 | 609 | sub http_req ($$$) { 610 | my ($uri, $req_body, $no_retry_statuses) = @_; 611 | 612 | my $err; 613 | 614 | for my $i (1 .. $MAX_HTTP_TRIES) { 615 | # send request 616 | $req->uri("$uri_prefix$uri"); 617 | 618 | $req->method($req_body ? "PUT" : "GET"); 619 | 620 | if ($req_body) { 621 | $req->content($req_body); 622 | } 623 | 624 | my $resp = $ua->request($req); 625 | 626 | # check the outcome 627 | if (!$resp->is_success) { 628 | $err = "request to $uri failed: " . $resp->status_line . ": " 629 | . ($resp->decoded_content || ""); 630 | 631 | my $status = $resp->code; 632 | if ($status == 400 633 | || (defined $no_retry_statuses 634 | && $no_retry_statuses->{$status})) 635 | { 636 | err_log $err; 637 | return undef, $err; 638 | } 639 | 640 | err_log "attempt $i of $MAX_HTTP_TRIES: $err"; 641 | 642 | sleep $i * 0.001; 643 | next; 644 | } 645 | 646 | my $body = $resp->decoded_content; 647 | my $data; 648 | eval { 649 | $data = $json_xs->decode($body); 650 | }; 651 | 652 | if ($@) { 653 | err_log "failed to decode JSON data $body for uri $uri: $@"; 654 | sleep $i * 0.001; 655 | next; 656 | } 657 | 658 | return $data; 659 | } 660 | 661 | return undef, $err 662 | } 663 | 664 | sub shell (@) { 665 | if (system(@_) != 0) { 666 | my $cmd = join(" ", map { /\s/ ? "'$_'" : $_ } @_); 667 | err_log "failed to run the command \"$cmd\": $?"; 668 | return undef; 669 | } 670 | 671 | return 1; 672 | } 673 | 674 | sub read_ini ($) { 675 | my $infile = shift; 676 | 677 | my $in; 678 | if (!open $in, $infile) { 679 | return undef, "cannot open $infile for reading: $!"; 680 | } 681 | 682 | my %sections; 683 | my $sec_name = 'default'; 684 | my $sec = ($sections{$sec_name} = {}); 685 | 686 | local $_; 687 | while (<$in>) { 688 | next if /^\s*$/ || /^\s*[\#;]/; 689 | 690 | if (/^ \s* (\w+) \s* = \s* (.*)/x) { 691 | my ($key, $val) = ($1, $2); 692 | $val =~ s/\s+$//; 693 | if (exists $sec->{$key}) { 694 | return undef, "$infile: line $.: duplicate key in section " 695 | . "\"$sec_name\": $key\n"; 696 | } 697 | $sec->{$key} = $val; 698 | next; 699 | } 700 | 701 | if (/^ \s* \[ \s* ([^\]]*) \] \s* $/x) { 702 | my $name = $1; 703 | $name =~ s/\s+$//; 704 | if ($name eq '') { 705 | return undef, "$infile: line $.: section name empty"; 706 | } 707 | 708 | if (exists $sections{$name}) { 709 | return undef, "$infile: line $.: section \"$name\" redefined"; 710 | } 711 | 712 | $sec = {}; 713 | $sections{$name} = $sec; 714 | $sec_name = $name; 715 | 716 | next; 717 | } 718 | 719 | return undef, "$infile: line $.: syntax error: $_"; 720 | } 721 | 722 | close $in; 723 | 724 | return \%sections; 725 | } 726 | 727 | sub parse_deps { 728 | my ($line, $file) = @_; 729 | 730 | my @items = split /\s*,\s*/, $line; 731 | my @parsed; 732 | for my $item (@items) { 733 | if ($item =~ m{^ ([-/\w]+) $}x) { 734 | my $full_name = $item; 735 | 736 | my ($account, $name); 737 | 738 | if ($full_name =~ m{^ ([-\w]+) / ([-\w]+) }x) { 739 | ($account, $name) = ($1, $2); 740 | 741 | } elsif ($full_name =~ $SpecialDepPat) { 742 | $name = $full_name; 743 | 744 | } else { 745 | return undef, "$file: bad dependency name: $full_name"; 746 | } 747 | 748 | push @parsed, [$account, $name]; 749 | 750 | } elsif ($item =~ m{^ ([-/\w]+) \s* ([^\w\s]+) \s* (\w\S*) $}x) { 751 | my ($full_name, $op, $ver) = ($1, $2, $3); 752 | 753 | my ($account, $name); 754 | 755 | if ($full_name =~ m{^ ([-\w]+) / ([-\w]+) }x) { 756 | ($account, $name) = ($1, $2); 757 | 758 | } elsif ($full_name =~ $SpecialDepPat) { 759 | $name = $full_name; 760 | 761 | } else { 762 | return undef, "$file: bad dependency name: $full_name"; 763 | } 764 | 765 | if ($op !~ /^ (?: >= | = | > ) $/x) { 766 | return undef, "$file: bad dependency version comparison" 767 | . " operator in \"$item\": $op"; 768 | } 769 | 770 | if ($ver !~ /\d/ || $ver =~ /[^-.\w]/) { 771 | return undef, "$file: bad version number in dependency" 772 | . " specification in \"$item\": $ver"; 773 | } 774 | 775 | push @parsed, [$account, $name, $op, $ver]; 776 | 777 | } else { 778 | return undef, "$file: bad dependency specification: $item"; 779 | } 780 | } 781 | 782 | @parsed = sort { $a->[1] cmp $b->[1] } @parsed; 783 | return \@parsed; 784 | } 785 | 786 | sub write_pid_file { 787 | my $pid = shift; 788 | open my $out, ">$pid_file" 789 | or die "cannot open $pid_file for writing: $!\n"; 790 | print $out $pid; 791 | close $out; 792 | } 793 | 794 | sub err_log { 795 | my @args = @_; 796 | my $ts = gen_timestamp(); 797 | warn "[$ts] ", @args; 798 | } 799 | 800 | sub shell_quote ($) { 801 | my $ret = shift; 802 | 803 | if (!defined $ret or $ret eq '') { 804 | return "''"; 805 | } 806 | 807 | if ($ret =~ /[[:cntrl:]]/) { 808 | die "shell_quote(): No way to quote string containing control characters\n"; 809 | } 810 | 811 | $ret =~ s/([#&;`'"|*?~!<>^()\[\]{}\$\\, ])/\\$1/g; 812 | 813 | return $ret; 814 | } 815 | -------------------------------------------------------------------------------- /web/conf/config.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | env = "dev" 3 | deferred_deletion_days=3 4 | 5 | [db] 6 | host = 127.0.0.1 7 | port = 5432 8 | max_idle_timeout = 60 9 | pool_size = 5 10 | database = "opm" 11 | user = "opm" 12 | password = "buildecosystem" 13 | 14 | [github_oauth] 15 | client_id=abc123 16 | client_secret=abc123 17 | host=https://github.com -------------------------------------------------------------------------------- /web/conf/get-limiting.conf: -------------------------------------------------------------------------------- 1 | limit_req zone=fetch_req burst=50; 2 | limit_conn upload_conn 4; 3 | -------------------------------------------------------------------------------- /web/conf/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | 12 | text/mathml mml; 13 | text/plain txt; 14 | text/vnd.sun.j2me.app-descriptor jad; 15 | text/vnd.wap.wml wml; 16 | text/x-component htc; 17 | 18 | image/png png; 19 | image/tiff tif tiff; 20 | image/vnd.wap.wbmp wbmp; 21 | image/x-icon ico; 22 | image/x-jng jng; 23 | image/x-ms-bmp bmp; 24 | image/svg+xml svg svgz; 25 | image/webp webp; 26 | 27 | application/font-woff woff; 28 | application/java-archive jar war ear; 29 | application/json json; 30 | application/mac-binhex40 hqx; 31 | application/msword doc; 32 | application/pdf pdf; 33 | application/postscript ps eps ai; 34 | application/rtf rtf; 35 | application/vnd.apple.mpegurl m3u8; 36 | application/vnd.ms-excel xls; 37 | application/vnd.ms-fontobject eot; 38 | application/vnd.ms-powerpoint ppt; 39 | application/vnd.wap.wmlc wmlc; 40 | application/vnd.google-earth.kml+xml kml; 41 | application/vnd.google-earth.kmz kmz; 42 | application/x-7z-compressed 7z; 43 | application/x-cocoa cco; 44 | application/x-java-archive-diff jardiff; 45 | application/x-java-jnlp-file jnlp; 46 | application/x-makeself run; 47 | application/x-perl pl pm; 48 | application/x-pilot prc pdb; 49 | application/x-rar-compressed rar; 50 | application/x-redhat-package-manager rpm; 51 | application/x-sea sea; 52 | application/x-shockwave-flash swf; 53 | application/x-stuffit sit; 54 | application/x-tcl tcl tk; 55 | application/x-x509-ca-cert der pem crt; 56 | application/x-xpinstall xpi; 57 | application/xhtml+xml xhtml; 58 | application/xspf+xml xspf; 59 | application/zip zip; 60 | 61 | application/octet-stream bin exe dll; 62 | application/octet-stream deb; 63 | application/octet-stream dmg; 64 | application/octet-stream iso img; 65 | application/octet-stream msi msp msm; 66 | 67 | application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; 68 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; 69 | application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; 70 | 71 | audio/midi mid midi kar; 72 | audio/mpeg mp3; 73 | audio/ogg ogg; 74 | audio/x-m4a m4a; 75 | audio/x-realaudio ra; 76 | 77 | video/3gpp 3gpp 3gp; 78 | video/mp2t ts; 79 | video/mp4 mp4; 80 | video/mpeg mpeg mpg; 81 | video/quicktime mov; 82 | video/webm webm; 83 | video/x-flv flv; 84 | video/x-m4v m4v; 85 | video/x-mng mng; 86 | video/x-ms-asf asx asf; 87 | video/x-ms-wmv wmv; 88 | video/x-msvideo avi; 89 | } 90 | -------------------------------------------------------------------------------- /web/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | master_process on; 2 | worker_processes auto; 3 | worker_cpu_affinity auto; 4 | 5 | error_log logs/error.log warn; 6 | #error_log logs/error.log debug; 7 | 8 | http { 9 | log_format main '$http_x_forwarded_for - $remote_addr - $remote_user [$time_local] $http_host "$request" $status $body_bytes_sent $request_time "$http_referer" "$http_user_agent"'; 10 | 11 | access_log logs/access.log main; 12 | 13 | lua_package_path "$prefix/lua/?.lua;$prefix/lua/vendor/?.lua;;"; 14 | 15 | lua_socket_connect_timeout 10s; 16 | lua_socket_read_timeout 10s; 17 | lua_socket_send_timeout 10s; 18 | 19 | resolver 8.8.4.4 ipv6=off; 20 | #lua_code_cache off; 21 | 22 | limit_req_zone $binary_remote_addr zone=fetch_req:50m rate=50r/s; 23 | 24 | limit_req_zone $binary_remote_addr zone=upload_req:50m rate=2r/s; 25 | limit_conn_zone $binary_remote_addr zone=upload_conn:50m; 26 | 27 | lua_shared_dict global_github_limit 100k; 28 | lua_shared_dict bad_users 50m; 29 | 30 | server_tokens off; 31 | 32 | init_by_lua_block { 33 | require "resty.core" 34 | require("opmserver") 35 | require("opmserver.init").init() 36 | } 37 | 38 | init_worker_by_lua_block { 39 | require("opmserver.init").init_worker() 40 | } 41 | 42 | server { 43 | server_name opm.openresty.org; 44 | 45 | # production deployment should use https here instead. 46 | listen 8080; 47 | 48 | lua_ssl_trusted_certificate root-ca.crt; 49 | lua_ssl_verify_depth 5; 50 | lua_ssl_protocols TLSv1.2; 51 | 52 | client_max_body_size 0; 53 | default_type text/plain; 54 | include mime.types; 55 | charset utf-8; 56 | 57 | location = /robots.txt { 58 | content_by_lua_block { 59 | ngx.say('User-agent: *\nDisallow: /api/'); 60 | } 61 | } 62 | 63 | location = /api/pkg/upload { 64 | client_max_body_size 256k; 65 | client_body_in_file_only on; 66 | 67 | default_type text/plain; 68 | 69 | # you should comment these out before doing load testing 70 | # and benchmark. 71 | limit_req zone=upload_req burst=5; 72 | limit_conn upload_conn 4; 73 | 74 | content_by_lua_block { 75 | require("opmserver").do_upload() 76 | } 77 | } 78 | 79 | location = /api/pkg/exists { 80 | expires 7d; 81 | 82 | include get-limiting.conf; 83 | 84 | content_by_lua_block { 85 | require("opmserver").do_pkg_exists() 86 | } 87 | } 88 | 89 | location = /api/pkg/fetch { 90 | expires 3m; 91 | 92 | include get-limiting.conf; 93 | 94 | content_by_lua_block { 95 | require("opmserver").do_pkg_fetch() 96 | } 97 | } 98 | 99 | location /api/pkg/tarball { 100 | expires 7d; 101 | 102 | set $opm_final_dir ''; 103 | 104 | include get-limiting.conf; 105 | 106 | access_by_lua_block { 107 | ngx.var.opm_final_dir = require("opmserver").get_final_directory() 108 | } 109 | 110 | alias $opm_final_dir; 111 | } 112 | 113 | location = /api/pkg/search { 114 | expires 5m; 115 | 116 | include get-limiting.conf; 117 | 118 | content_by_lua_block { 119 | require("opmserver").do_pkg_search() 120 | } 121 | } 122 | 123 | location = /api/pkg/search/name { 124 | expires 5m; 125 | 126 | include get-limiting.conf; 127 | 128 | content_by_lua_block { 129 | require("opmserver").do_pkg_name_search() 130 | } 131 | } 132 | 133 | # only for internal use in util/opm-pkg-indexer.pl 134 | location = /api/pkg/incoming { 135 | allow 127.0.0.1; 136 | deny all; 137 | 138 | content_by_lua_block { 139 | require("opmserver").do_incoming() 140 | } 141 | } 142 | 143 | # only for internal use in util/opm-pkg-indexer.pl 144 | location = /api/pkg/processed { 145 | allow 127.0.0.1; 146 | deny all; 147 | 148 | content_by_lua_block { 149 | require("opmserver").do_processed() 150 | } 151 | } 152 | 153 | location = /favicon.ico { 154 | expires 1d; 155 | alias images/favicon.ico; 156 | } 157 | 158 | location /css { 159 | expires 1d; 160 | alias css; 161 | } 162 | 163 | 164 | location /js { 165 | expires 1d; 166 | alias js; 167 | } 168 | 169 | location /images { 170 | expires 1d; 171 | alias images; 172 | } 173 | 174 | location /api/ { 175 | default_type application/json; 176 | 177 | content_by_lua_block { 178 | require("opmserver").do_web() 179 | } 180 | } 181 | 182 | location / { 183 | default_type text/html; 184 | expires 10s; 185 | 186 | content_by_lua_block { 187 | require("opmserver").do_web() 188 | } 189 | } 190 | } 191 | } 192 | 193 | events { 194 | accept_mutex off; 195 | } 196 | -------------------------------------------------------------------------------- /web/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #3e423e; 3 | font-size: 16px; 4 | font-family: "Open Sans", sans-serif; 5 | margin: 0; } 6 | 7 | a { 8 | color: #4d8d89; 9 | text-decoration:none } 10 | a:hover { 11 | color: #325c59; 12 | text-decoration:underline; } 13 | 14 | h1, h2, h3 { 15 | margin: 40px 0 20px 0; } 16 | 17 | h2 { 18 | font-size: 24px; } 19 | 20 | h3 { 21 | font-size: 18px; } 22 | 23 | p { 24 | line-height: 1.2; 25 | margin: 10px 0; } 26 | p > code { 27 | background: #f5f4f4; 28 | padding: 2px 4px; 29 | border-radius: 6px; } 30 | 31 | b { 32 | color: #5E5E5E; 33 | font-weight: bold; } 34 | 35 | hr { 36 | height: 1px; 37 | background: #C1CCE4; 38 | border: 0; 39 | margin: 20px 0; } 40 | 41 | .nv { 42 | color: #ff9898; } 43 | 44 | input { 45 | font-family: 'Open Sans', sans-serif; 46 | font-size: 16px; } 47 | 48 | .header { 49 | background: #446D44; 50 | height: 55px; 51 | color: white; } 52 | .header .header_inner { 53 | max-width: 960px; 54 | min-width: 800px; 55 | margin: 0 auto; 56 | height: 100%; 57 | display: flex; 58 | align-items: center; } 59 | .header .header_inner > * { 60 | margin: 0 10px; } 61 | .header .header_inner > *:last-child { 62 | margin-right: 0; } 63 | .header .header_inner > *:first-child { 64 | margin-left: 0; } 65 | .header .header_search { 66 | flex: 1; 67 | max-width: 400px; } 68 | .header .header_search input[type=text] { 69 | border: 0; 70 | font-size: 14px; 71 | padding: 8px 10px; 72 | border-radius: 16px; 73 | background: #eff4ff; 74 | color: #666; 75 | width: 100%; 76 | box-sizing: border-box; } 77 | .header .header_search input[type=text]:focus { 78 | outline: none; 79 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); } 80 | .header .logo_panel { 81 | font-size: 20px; 82 | 83 | text-align: left; 84 | display: flex; 85 | 86 | align-content: left; } 87 | .header .logo_panel a { 88 | color: #edf1ed; 89 | text-decoration: none; 90 | font-weight: bold; } 91 | .header .logo_panel a:hover { 92 | color: #eef8ff; } 93 | 94 | .header .nav_panel { 95 | font-size: 15px; 96 | flex: 1; 97 | text-align: right; 98 | display: flex; 99 | justify-content: flex-end; 100 | align-content: center; } 101 | .header .nav_panel > * { 102 | margin-left: 12px; } 103 | .header .nav_panel > *:first-child { 104 | margin-left: 0; } 105 | .header .nav_panel a { 106 | color: #E8E8E8; 107 | text-decoration: none; 108 | font-weight: bold; } 109 | .header .nav_panel a:hover { 110 | color: #8CE3FF; } 111 | .header .text_logo { 112 | width: 150px; 113 | vertical-align: top; } 114 | .header .icon_logo { 115 | width: 45px; 116 | height: 45px; 117 | margin-top: 2px; } 118 | 119 | .content { 120 | background: #f9fcf9; 121 | border-bottom: 1px solid #d3dbec; } 122 | 123 | .main_col { 124 | max-width: 960px; 125 | min-width: 800px; 126 | margin: 0 auto; 127 | padding-bottom: 40px; } 128 | 129 | .footer { 130 | color: #9E9E9E; 131 | width: 960px; 132 | margin: 0 auto; 133 | margin-top: 20px; 134 | margin-bottom: 40px; 135 | text-align: center; 136 | font-size: 14px; } 137 | .footer a { 138 | text-decoration: underline; } 139 | .footer .right { 140 | float: right; } 141 | 142 | pre { 143 | background: #f5f4f4; 144 | color: #24292e; 145 | font-size: 16px; 146 | padding: 16px 24px; 147 | border-radius: 2px; } 148 | pre.term_snippet { 149 | font-size: 14px; } 150 | 151 | .pager { 152 | text-align: center; 153 | font-size: 14px; 154 | padding: 10px 0; 155 | display: inline-block } 156 | 157 | .pager a,.pager span { 158 | display: inline-block; 159 | padding: 0 10px; 160 | height: 28px; 161 | line-height: 28px; 162 | border: 1px solid #ddd; 163 | border-radius: 3px; 164 | text-decoration: none; 165 | color: #999; 166 | cursor: pointer; 167 | margin-right:2px; } 168 | .pager a:hover:not(.disabled):not(.current), .pager span:hover:not(.disabled):not(.current) { 169 | color: #fff; 170 | background: #4d8d89; } 171 | .pager a.disabled,.pager span.disabled { 172 | color: #bfbfbf; 173 | background: #f5f5f5; 174 | cursor: no-drop; 175 | border: 1px solid #ddd; } 176 | .pager a.current,.pager span.current { 177 | color: #fff; 178 | background: #4d8d89; 179 | border: 1px solid #fff; } 180 | 181 | .main_page .split_header { 182 | 183 | align-items: center; 184 | margin: 20px 0 20px 0; } 185 | .main_page .split_header h2 { 186 | display: inline; 187 | margin-top: 0; 188 | margin-bottom: 0; } 189 | .main_page .split_header > * { 190 | margin-right: 10px; } 191 | .main_page .split_header .right { 192 | margin-top: 8px; 193 | margin-right: 2px; 194 | float: right; 195 | font-size: 18px; 196 | } 197 | .main_page .split_header .user_github { 198 | margin-top: 2px; 199 | margin-right: 2px; 200 | float: right; 201 | font-size: 18px; 202 | } 203 | 204 | .main_page .header_sub { 205 | font-size: 14px; 206 | margin-left: 5px; 207 | color: #919294; 208 | } 209 | .main_page .header_sub a { 210 | color: #757779; } 211 | 212 | .main_page .intro_banner { 213 | background: white; 214 | border-bottom: 1px solid #d3dbec; 215 | margin-bottom: 30px; 216 | padding: 25px 0; } 217 | .main_page .intro_banner .intro_banner_inner { 218 | width: 960px; 219 | margin: 0 auto; } 220 | .main_page .intro_banner img { 221 | float: left; 222 | width: 130px; 223 | height: 124px; } 224 | .main_page .intro_banner .intro_text { 225 | margin-left: 15px; } 226 | .main_page .intro_banner p { 227 | margin: 10px 0; } 228 | .main_page .intro_banner p:first-child { 229 | margin-top: 0; } 230 | .main_page .intro_banner p:last-child { 231 | margin-bottom: 0; } 232 | 233 | .package_list { 234 | list-style: none; 235 | margin: 0; 236 | padding: 0; } 237 | .package_list .package_row { 238 | margin: 10px 0; 239 | background: white; 240 | padding: 5px 10px; 241 | border-radius: 2px; 242 | border: 1px solid #C1CCE4; 243 | line-height: 1.5; } 244 | .package_list .package_row .title { 245 | font-size: 18px; } 246 | .package_list .package_row .author { 247 | color: gray; 248 | float: right; } 249 | .package_list .package_row .author a { 250 | color: #444; } 251 | .package_list .package_row .author a:hover { 252 | color: #666; } 253 | .package_list .package_row .main, .package_list .package_row .summary { 254 | white-space: nowrap; 255 | overflow: hidden; 256 | text-overflow: ellipsis; } 257 | .package_list .package_row .summary { 258 | color: gray; 259 | margin-top: 5px; 260 | font-size: 15px; } 261 | .package_list .package_row .version_name { 262 | color: gray; 263 | margin-left: 2px; 264 | font-size: 16px; } 265 | .package_list .package_row .failed { 266 | color: red; 267 | margin-left: 2px; 268 | font-size: 16px; } 269 | .package_list .package_row .pending { 270 | color: purple; 271 | margin-left: 2px; 272 | font-size: 16px; } 273 | .package_list .package_row .updated_at { 274 | color: gray; 275 | float: right; 276 | margin-right: 5px; 277 | font-size: 14px; } 278 | 279 | .metadata_columns { 280 | margin-top: 25px; 281 | border: 1px solid #d3dbec; 282 | white-space: nowrap; } 283 | .metadata_columns .metadata_columns_inner { 284 | max-width: 960px; 285 | min-width: 800px; 286 | margin: 0 auto; } 287 | .metadata_columns .column { 288 | box-sizing: border-box; 289 | white-space: normal; 290 | height: 75px; 291 | margin-right: 40px; 292 | border-left: 1px solid #d3dbec; 293 | display: inline-block; 294 | vertical-align: top; 295 | font-size: 18px; 296 | padding: 15px 0 15px 20px; } 297 | .metadata_columns .column h3 { 298 | margin-top: 0; 299 | margin-bottom: 3px; 300 | font-size: 12px; 301 | color: #919294; 302 | font-weight: bold; 303 | text-transform: uppercase; } 304 | 305 | .package_page h3 { 306 | font-weight: bold; } 307 | 308 | .package_page .description { 309 | margin-top: 30px; } 310 | 311 | .package_page .installer { 312 | width: 960px; 313 | margin: 15px auto; } 314 | .package_page .installer pre { 315 | background: #323744; 316 | color: white; 317 | margin: 0; 318 | position: relative; 319 | padding-left: 95px; } 320 | .package_page .installer pre:before { 321 | content: "Install:"; 322 | font-family: "Open Sans", sans-serif; 323 | font-size: 12px; 324 | text-transform: uppercase; 325 | font-weight: bold; 326 | position: absolute; 327 | top: 0; 328 | bottom: 0; 329 | left: 0; 330 | line-height: 48px; 331 | padding: 0 10px; 332 | display: inline-block; 333 | background: rgba(255, 255, 255, 0.2); 334 | color: rgba(255, 255, 255, 0.6); } 335 | 336 | .error_info { 337 | color: red; 338 | margin-left: 2px; 339 | font-size: 18px; } 340 | 341 | .sign-in { 342 | margin: 0 auto; 343 | text-align: center; } 344 | 345 | .button { 346 | display: inline-block; 347 | text-decoration: none; 348 | background: #4d8d89; 349 | line-height: 35px; 350 | height: 35px; 351 | padding: 0 30px; 352 | font-size: 16px; 353 | font-family: 'Open Sans', sans-serif; 354 | border: 0; 355 | border-radius: 5px; 356 | color: #FFF; 357 | cursor: pointer; 358 | } 359 | 360 | .delete-btn, .cancel-deleting-btn{ 361 | width: 1rem !important; 362 | height: 1rem !important; 363 | background-position: 0 15px; 364 | } 365 | 366 | .delete-pkg, .cancel-deleting-pkg{ 367 | overflow: hidden; 368 | } 369 | 370 | .delete-pkg:hover img{ 371 | transform: translateY(-50px); 372 | filter: drop-shadow(#AF0C0C 0 50px); 373 | } 374 | 375 | .cancel-deleting-pkg:hover img{ 376 | transform: translateY(-50px); 377 | filter: drop-shadow(#AF0C0C 0 50px); 378 | } 379 | -------------------------------------------------------------------------------- /web/email.ini.example: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | # mailgun login name 4 | login=postmaster@opm.openresty.org 5 | 6 | # mailgun password 7 | password=foobarbazblah1234 8 | -------------------------------------------------------------------------------- /web/images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openresty/opm/315e56b907eb5edefa704df0b0fe286998e0317e/web/images/cancel.png -------------------------------------------------------------------------------- /web/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openresty/opm/315e56b907eb5edefa704df0b0fe286998e0317e/web/images/delete.png -------------------------------------------------------------------------------- /web/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openresty/opm/315e56b907eb5edefa704df0b0fe286998e0317e/web/images/favicon.ico -------------------------------------------------------------------------------- /web/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openresty/opm/315e56b907eb5edefa704df0b0fe286998e0317e/web/images/github.png -------------------------------------------------------------------------------- /web/lua/opmserver/conf.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local resty_ini = require "resty.ini" 4 | 5 | local config 6 | local prefix = ngx.config.prefix() 7 | 8 | do 9 | config = assert(resty_ini.parse_file(prefix .. "conf/config.ini")) 10 | end 11 | 12 | 13 | function _M.load_entry(key) 14 | return config[key] 15 | end 16 | 17 | 18 | function _M.get_config() 19 | return config 20 | end 21 | 22 | 23 | return _M 24 | -------------------------------------------------------------------------------- /web/lua/opmserver/crontab.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | 4 | local log = require("opmserver.log") 5 | local query_db = require("opmserver.db").query 6 | local common_conf = require("opmserver.conf").load_entry("common") 7 | local run_shell = require("resty.shell").run 8 | local shell_escape = require("opmserver.utils").shell_escape 9 | 10 | 11 | local ngx = ngx 12 | local ngx_time = ngx.time 13 | local timer_every = ngx.timer.every 14 | 15 | 16 | local env = common_conf.env 17 | local deferred_deletion_days = common_conf.deferred_deletion_days or 3 18 | local deferred_deletion_seconds = deferred_deletion_days * 86400 19 | 20 | 21 | local function move_pkg_files(pkg) 22 | local package_name = shell_escape(pkg.package_name) 23 | local uploader_name = shell_escape(pkg.uploader_name) 24 | local version = pkg.version_s 25 | local tar_path = uploader_name .. "/" .. package_name 26 | .. "-" .. version .. ".opm.tar.gz" 27 | local final_path = "/opm/final/" .. tar_path 28 | local bak_path = "/opm/bak/" .. tar_path 29 | 30 | local cmd = {"mkdir", "-p", "/opm/bak/" .. uploader_name} 31 | local ok, stdout, stderr, reason, status = run_shell(cmd) 32 | if not ok then 33 | log.error("failed to make dir, stdout: ", stdout, 34 | ", stderr: ", stderr, 35 | ", reason: ", reason, ", status: ", status) 36 | 37 | return nil, stderr or reason or "failed to run shell" 38 | end 39 | 40 | cmd = {"mv", final_path, bak_path} 41 | ok, stdout, stderr, reason, status = run_shell(cmd) 42 | if not ok then 43 | log.error("failed to mv file, stdout: ", stdout, 44 | ", stderr: ", stderr, 45 | ", reason: ", reason, ", status: ", status) 46 | 47 | return nil, stderr or reason or "failed to run shell" 48 | end 49 | 50 | return true 51 | end 52 | 53 | 54 | local function check_deferred_deletions() 55 | local sql = [[select uploads.id as pkg_id, package_name, indexed]] 56 | .. [[, version_s, deleted_at_time]] 57 | .. [[, failed, users.login as uploader_name]] 58 | .. [[, orgs.login as org_name from uploads]] 59 | .. [[ left join users on uploads.uploader = users.id]] 60 | .. [[ left join orgs on uploads.org_account = orgs.id]] 61 | .. [[ where uploads.is_deleted = true]] 62 | local res, err = query_db(sql) 63 | if not res then 64 | log.error("get deferred deletions failed: ", err) 65 | return nil, err 66 | end 67 | 68 | local curr_time = ngx_time() 69 | local pkg_list = res 70 | for _, pkg in ipairs(pkg_list) do 71 | local deleted_at_time = pkg.deleted_at_time 72 | if deleted_at_time + deferred_deletion_seconds <= curr_time then 73 | local pkg_id = pkg.pkg_id 74 | sql = "delete from uploads where id = " .. pkg_id 75 | local res, err = query_db(sql) 76 | if not res then 77 | log.error("delete pkg [", pkg_id, "] failed: ", err) 78 | return 79 | end 80 | 81 | local ok, err = move_pkg_files(pkg) 82 | if not ok then 83 | log.error('move pkg files failed: ', err) 84 | return 85 | end 86 | 87 | log.warn("delete pkg successfully: pkg name:", pkg.package_name, 88 | ", version:", pkg.version_s, 89 | ", uploader: ", pkg.uploader_name) 90 | end 91 | end 92 | end 93 | 94 | 95 | local jobs = { 96 | { 97 | interval = (env == 'dev') and 10 or 120, 98 | handler = check_deferred_deletions, 99 | }, 100 | } 101 | 102 | local function timer_handler(premature) 103 | if premature then 104 | return 105 | end 106 | 107 | for _, job in ipairs(jobs) do 108 | local next_time = job.next_time 109 | local now = ngx_time() 110 | 111 | if not next_time then 112 | if job.initiate then 113 | -- crontab job will start after at 3-10 seconds 114 | next_time = math.random(3, 10) + now 115 | 116 | else 117 | -- crontab job will start after at least 10 seconds 118 | next_time = math.random(10, job.interval) + now 119 | end 120 | 121 | job.next_time = next_time 122 | job.running_num = 0 123 | end 124 | 125 | if next_time <= now 126 | and (not job.concurrency or job.running_num < job.concurrency) 127 | then 128 | job.next_time = now + job.interval 129 | 130 | job.running_num = job.running_num + 1 131 | 132 | local ok, err = pcall(job.handler) 133 | if not ok then 134 | log.error("failed to run crontab job: ", err) 135 | end 136 | 137 | job.running_num = job.running_num - 1 138 | 139 | if ngx.worker.exiting() then 140 | break 141 | end 142 | end 143 | end 144 | end 145 | 146 | 147 | function _M.init_worker() 148 | local ok, err = timer_every(1, timer_handler) 149 | if not ok then 150 | log.crit("failed to create every timer: ", err) 151 | end 152 | end 153 | 154 | 155 | return _M 156 | -------------------------------------------------------------------------------- /web/lua/opmserver/db.lua: -------------------------------------------------------------------------------- 1 | local pgmoon = require "pgmoon" 2 | local db_conf = require("opmserver.conf").load_entry("db") 3 | 4 | local ngx = ngx 5 | local null = ngx.null 6 | local set_quote_sql_str = ndk.set_var.set_quote_pgsql_str 7 | 8 | local _M = {} 9 | 10 | local idle_timeout = (db_conf.max_idle_timeout or 60) * 1000 11 | local pool_size = db_conf.pool_size or 5 12 | 13 | 14 | function _M.quote(v) 15 | if not v or v == null then 16 | return "null" 17 | end 18 | local typ = type(v) 19 | if typ == "number" or typ == "boolean" then 20 | return tostring(v) 21 | end 22 | return set_quote_sql_str(v) 23 | end 24 | 25 | 26 | function _M.query(sql) 27 | local pg = pgmoon.new(db_conf) 28 | 29 | local ok, err = pg:connect() 30 | if not ok then 31 | return nil, ("failed to connect pg: " .. err or "") 32 | end 33 | 34 | local result, err, partial, num_queries = pg:query(sql) 35 | if not result and err then 36 | ngx.log(ngx.ERR, "failed to query sql, err: ", err, ", sql: ", sql) 37 | pg:disconnect() 38 | return nil, err 39 | end 40 | 41 | local ok, err = pg:keepalive(idle_timeout, pool_size) 42 | if not ok then 43 | ngx.log(ngx.ERR, "pg keepalive failed: ", err) 44 | end 45 | 46 | return result, nil, partial, num_queries 47 | end 48 | 49 | 50 | return _M 51 | -------------------------------------------------------------------------------- /web/lua/opmserver/email.lua: -------------------------------------------------------------------------------- 1 | local resty_ini = require "resty.ini" 2 | 3 | 4 | local ngx = ngx 5 | local find = ngx.re.find 6 | local gsub = ngx.re.gsub 7 | local sub = string.sub 8 | local tostring = tostring 9 | local tonumber = tonumber 10 | local encode_base64 = ngx.encode_base64 11 | local socket_tcp = ngx.socket.tcp 12 | 13 | 14 | local account, password, sender_addr 15 | local sender_name = "OPM Indexer" 16 | 17 | 18 | local _M = { 19 | } 20 | 21 | 22 | local function send_cmd(sock, cmd) 23 | if not account then 24 | local ini_file = ngx.config.prefix() .. "email.ini" 25 | 26 | local conf, err = resty_ini.parse_file(ini_file) 27 | if not conf then 28 | return nil, ini_file .. ": failed to load ini file: " .. err 29 | end 30 | 31 | local default = conf.default 32 | 33 | account = default.account 34 | if not account or account == "" then 35 | return nil, ini_file .. ": key \"account\" not found" 36 | end 37 | 38 | -- ngx.log(ngx.WARN, "account: ", account) 39 | 40 | password = default.password 41 | if not password or password == "" then 42 | return nil, ini_file .. ": key \"password\" not found" 43 | end 44 | 45 | -- ngx.log(ngx.WARN, "password: ", password) 46 | 47 | sender_addr = default.sender_addr 48 | if not sender_addr or sender_addr == "" then 49 | return nil, ini_file .. ": key \"sender_addr\" not found" 50 | end 51 | end 52 | 53 | -- ngx.log(ngx.WARN, "> ", cmd) 54 | 55 | local bytes, err = sock:send(cmd .. "\r\n") 56 | if not bytes then 57 | return bytes, err 58 | end 59 | 60 | do 61 | local lines = {} 62 | local i = 0 63 | local line, err = sock:receive() 64 | if not line then 65 | return nil, err 66 | end 67 | 68 | while true do 69 | local fr, to, err = find(line, [[^(\d+)-]], "jo", nil, 1) 70 | if err then 71 | return nil, "failed to call ngx.re.find: " .. tostring(err) 72 | end 73 | if not fr then 74 | break 75 | end 76 | local status = tonumber(sub(line, fr, to)) 77 | if status >= 400 then 78 | return nil, line 79 | end 80 | i = i + 1 81 | lines[i] = line 82 | local err 83 | line, err = sock:receive() 84 | if not line then 85 | return nil, err 86 | end 87 | end 88 | 89 | local fr, to, err = find(line, [[^(\d+) ]], "jo", nil, 1) 90 | if err then 91 | return nil, "failed to call ngx.re.find: " .. tostring(err) 92 | end 93 | if not fr then 94 | return nil, "syntax error: " .. tostring(line) 95 | end 96 | local status = tonumber(sub(line, fr, to)) 97 | if status >= 400 then 98 | return nil, line 99 | end 100 | 101 | i = i + 1 102 | lines[i] = line 103 | -- ngx.log(ngx.WARN, table.concat(lines, "\n")) 104 | end 105 | 106 | return true 107 | end 108 | 109 | 110 | function _M.send_mail(recipient, recipient_name, mail_title, mail_body) 111 | local sock = socket_tcp() 112 | local host = "smtp.mailgun.org" 113 | 114 | local ok, err = sock:connect(host, 587) 115 | if not ok then 116 | return nil, "failed to connect to " .. (host or "") .. ":587: " 117 | .. (err or "") 118 | end 119 | 120 | local line, err = sock:receive() 121 | if not line then 122 | return nil, "failed to receive the first line from " 123 | .. (host or "") .. ":587: " .. (err or "") 124 | end 125 | -- ngx.log(ngx.WARN, line) 126 | 127 | ok, err = send_cmd(sock, "EHLO " .. host) 128 | if not ok then 129 | return nil, err 130 | end 131 | 132 | ok, err = send_cmd(sock, "STARTTLS") 133 | if not ok then 134 | return nil, err 135 | end 136 | 137 | local sess, err = sock:sslhandshake(nil, host, true) 138 | if not sess then 139 | return nil, err 140 | end 141 | 142 | ok, err = send_cmd(sock, "EHLO " .. host) 143 | if not ok then 144 | return nil, err 145 | end 146 | 147 | local auth = encode_base64("\0" .. account .. "\0" .. password) 148 | 149 | ok, err = send_cmd(sock, "AUTH PLAIN " .. auth) 150 | if not ok then 151 | return nil, err 152 | end 153 | 154 | local ok, err = send_cmd(sock, "MAIL FROM:<" .. sender_addr .. ">") 155 | if not ok then 156 | return nil, err 157 | end 158 | 159 | local ok, err = send_cmd(sock, "RCPT TO:<" .. recipient .. ">") 160 | if not ok then 161 | return nil, err 162 | end 163 | 164 | local bytes, err = sock:send{ 165 | "DATA\r\n", 166 | "FROM: ", sender_name, " <", sender_addr, ">\r\n", 167 | "Subject: ", mail_title, "\r\n", 168 | "To: ", recipient_name, " <", recipient, ">\r\n", 169 | "Content-Transfer-Encoding: base64\r\n", 170 | "Content-Type: text/plain; charset=utf-8\r\n\r\n", 171 | encode_base64(mail_body), "\n", 172 | } 173 | if not bytes then 174 | return nil, err 175 | end 176 | 177 | ok, err = send_cmd(sock, "\r\n.") 178 | if not ok then 179 | return nil, err 180 | end 181 | 182 | ok, err = send_cmd(sock, "QUIT") 183 | if not ok then 184 | return nil, err 185 | end 186 | 187 | ok, err = sock:close() 188 | if not ok then 189 | return nil, err 190 | end 191 | 192 | return true 193 | end 194 | 195 | 196 | return _M 197 | -------------------------------------------------------------------------------- /web/lua/opmserver/github_oauth.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local log = require("opmserver.log") 4 | local json_decode = require("cjson.safe").decode 5 | local json_encode = require("cjson.safe").encode 6 | local http = require "resty.http" 7 | local github_oauth = require("opmserver.conf").load_entry("github_oauth") 8 | local common_conf = require("opmserver.conf").load_entry("common") 9 | 10 | 11 | local ngx = ngx 12 | local ngx_var = ngx.var 13 | local str_find = string.find 14 | 15 | 16 | local client_id = github_oauth.client_id 17 | local client_secret = github_oauth.client_secret 18 | local env = common_conf.env 19 | local github_oauth_host = github_oauth.host 20 | 21 | 22 | local function http_request(url, params) 23 | local httpc = http.new() 24 | local res, err = httpc:request_uri(url, params) 25 | 26 | if not res then 27 | return nil, err or 'unknown' 28 | end 29 | 30 | if res.status ~= 200 and res.status ~= 201 then 31 | return nil, "response code: " .. res.status .. " body: " 32 | .. (res.body or nil) 33 | end 34 | 35 | log.info("response: ", res.body) 36 | 37 | if not res.body then 38 | return nil, 'response body is empty' 39 | end 40 | local data, err = json_decode(res.body) 41 | if not data then 42 | return nil, 'decode response body failed: ' .. err 43 | end 44 | 45 | return data 46 | end 47 | 48 | 49 | local function request_access_token(code) 50 | local url = github_oauth_host .. "/login/oauth/access_token" 51 | 52 | local res, err = http_request(url, { 53 | method = 'POST', 54 | headers = { 55 | ["Accept"] = "application/json", 56 | }, 57 | query = { 58 | client_id = client_id, 59 | client_secret = client_secret, 60 | code = code, 61 | }, 62 | ssl_verify = false, 63 | }) 64 | 65 | if not res then 66 | return nil, err 67 | end 68 | 69 | return res 70 | end 71 | 72 | 73 | local function get_user_info(access_token) 74 | local url = "https://api.github.com/user" 75 | 76 | local res, err = http_request(url, { 77 | method = 'GET', 78 | headers = { 79 | Accept = 'application/vnd.github.v3+json', 80 | Authorization = 'token ' .. access_token, 81 | }, 82 | query = { 83 | access_token = access_token, 84 | }, 85 | ssl_verify = false, 86 | }) 87 | 88 | return res, err 89 | end 90 | 91 | 92 | function _M.get_verified_email(access_token) 93 | local url = "https://api.github.com/user/emails" 94 | 95 | local data, err = http_request(url, { 96 | method = 'GET', 97 | headers = { 98 | Accept = 'application/vnd.github.v3+json', 99 | Authorization = 'token ' .. access_token, 100 | }, 101 | query = { 102 | access_token = access_token, 103 | }, 104 | ssl_verify = false, 105 | }) 106 | 107 | if not data then 108 | return nil, 'query user github email info failed: ' .. err 109 | end 110 | 111 | local email 112 | 113 | for _, item in ipairs(data) do 114 | if item.primary and item.verified then 115 | email = item.email 116 | break 117 | end 118 | end 119 | 120 | if not email then 121 | for _, item in ipairs(data) do 122 | if item.verified then 123 | email = item.email 124 | break 125 | end 126 | end 127 | end 128 | 129 | if not email then 130 | return nil, "no verified email address found from github: " 131 | .. json_encode(data) 132 | end 133 | 134 | return email 135 | end 136 | 137 | 138 | function _M.auth(code) 139 | if not code then 140 | return nil, "missing code" 141 | end 142 | 143 | local token_info, err = request_access_token(code) 144 | if err then 145 | return nil, 'request access_token failed: ' .. err 146 | end 147 | 148 | if not token_info or not token_info.access_token then 149 | return nil, 'get access_token failed' 150 | end 151 | 152 | log.info('token_info:', json_encode(token_info)) 153 | 154 | local scope = token_info.scope 155 | if not scope or not str_find(scope, "read:org", nil, true) then 156 | return nil, "personal access token lacking the read:org scope: " 157 | .. scope 158 | end 159 | 160 | local access_token = token_info.access_token 161 | local user_info, err = get_user_info(access_token) 162 | 163 | if not user_info then 164 | return nil, "request github user profile failed: " .. err 165 | end 166 | 167 | log.info("user_info: ", json_encode(user_info)) 168 | 169 | return user_info, nil, access_token 170 | end 171 | 172 | 173 | function _M.get_login_url() 174 | local state = "" 175 | local scheme = 'https' 176 | if env and env == 'dev' then 177 | scheme = 'http' 178 | end 179 | 180 | local redirect_url = scheme .. '://' .. ngx_var.http_host .. "/github_auth/" 181 | local url = "https://github.com/login/oauth/authorize?client_id=" 182 | .. client_id .. "&redirect_uri=" .. redirect_url 183 | .. "&scope=user:email,read:org&state=" .. state 184 | 185 | return url 186 | end 187 | 188 | 189 | return _M 190 | -------------------------------------------------------------------------------- /web/lua/opmserver/init.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | 4 | local log = require("opmserver.log") 5 | 6 | 7 | local ngx = ngx 8 | local ngx_log = ngx.log 9 | local ngx_ERR = ngx.ERR 10 | local require = require 11 | 12 | 13 | local function init_random() 14 | local frandom, err = io.open("/dev/urandom", "rb") 15 | if not frandom then 16 | log.crit("failed to open file /dev/urandom: ", err) 17 | 18 | math.randomseed(ngx.now()) 19 | 20 | else 21 | local str = frandom:read(4) 22 | frandom:close() 23 | local seed = 0 24 | for i = 1, 4 do 25 | seed = bit.bor(bit.lshift(seed, 8), str:byte(i)) 26 | end 27 | seed = bit.bxor(seed, ngx.worker.pid()) 28 | math.randomseed(seed) 29 | end 30 | end 31 | 32 | 33 | function _M.init() 34 | init_random() 35 | 36 | local process = require "ngx.process" 37 | local ok, err = process.enable_privileged_agent() 38 | if not ok then 39 | ngx_log(ngx_ERR, "enables privileged agent failed error:", err) 40 | end 41 | end 42 | 43 | 44 | function _M.init_worker(mode) 45 | if mode and mode == 'ci' then 46 | return 47 | end 48 | 49 | local process = require "ngx.process" 50 | if process.type() == 'privileged agent' then 51 | require("opmserver.crontab").init_worker() 52 | end 53 | end 54 | 55 | 56 | return _M 57 | -------------------------------------------------------------------------------- /web/lua/opmserver/log.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local cur_level = ngx.config.subsystem == "http" and 4 | require "ngx.errlog" .get_sys_filter_level() 5 | local ngx_log = ngx.log 6 | local ngx_CRIT = ngx.CRIT 7 | local ngx_ERR = ngx.ERR 8 | local ngx_WARN = ngx.WARN 9 | local ngx_INFO = ngx.INFO 10 | local ngx_NOTICE = ngx.NOTICE 11 | local ngx_DEBUG = ngx.DEBUG 12 | local DEBUG = ngx.config.debug 13 | 14 | 15 | -- NGX_LOG_STDERR 0 16 | -- NGX_LOG_EMERG 1 17 | -- NGX_LOG_ALERT 2 18 | -- NGX_LOG_CRIT 3 19 | -- NGX_LOG_ERR 4 20 | -- NGX_LOG_WARN 5 21 | -- NGX_LOG_NOTICE 6 22 | -- NGX_LOG_INFO 7 23 | -- NGX_LOG_DEBUG 8 24 | 25 | 26 | function _M.crit(...) 27 | if cur_level and ngx_CRIT > cur_level then 28 | return 29 | end 30 | 31 | return ngx_log(ngx_CRIT, ...) 32 | end 33 | 34 | 35 | function _M.error(...) 36 | if cur_level and ngx_ERR > cur_level then 37 | return 38 | end 39 | 40 | return ngx_log(ngx_ERR, ...) 41 | end 42 | 43 | 44 | function _M.warn(...) 45 | if cur_level and ngx_WARN > cur_level then 46 | return 47 | end 48 | 49 | return ngx_log(ngx_WARN, ...) 50 | end 51 | 52 | 53 | function _M.notice(...) 54 | if cur_level and ngx_NOTICE > cur_level then 55 | return 56 | end 57 | 58 | return ngx_log(ngx_NOTICE, ...) 59 | end 60 | 61 | 62 | function _M.info(...) 63 | if cur_level and ngx_INFO > cur_level then 64 | return 65 | end 66 | 67 | return ngx_log(ngx_INFO, ...) 68 | end 69 | 70 | 71 | function _M.debug(...) 72 | if not DEBUG and cur_level and ngx_DEBUG > cur_level then 73 | return 74 | end 75 | 76 | return ngx_log(ngx_DEBUG, ...) 77 | end 78 | 79 | 80 | return _M 81 | -------------------------------------------------------------------------------- /web/lua/opmserver/paginator.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | local mt = { __index = _M } 3 | 4 | local select = select 5 | local setmetatable = setmetatable 6 | local str_find = string.find 7 | local str_gsub = string.gsub 8 | local tab_concat = table.concat 9 | local modf = math.modf 10 | local ceil = math.ceil 11 | 12 | local function tab_append(t, idx, ...) 13 | local n = select("#", ...) 14 | for i = 1, n do 15 | t[idx + i] = select(i, ...) 16 | end 17 | return idx + n 18 | end 19 | 20 | function _M:new(url, total, page_size, page) 21 | total = total or 1 22 | page_size = page_size or 20 23 | 24 | page = page or 1 25 | if page < 1 then 26 | page = 1 27 | end 28 | 29 | local page_count = ceil(total / page_size) 30 | if page > page_count then 31 | page = page_count 32 | end 33 | 34 | local this = { 35 | total = total, 36 | page_size = page_size, 37 | page = page, 38 | page_count = page_count, 39 | url = url 40 | } 41 | 42 | setmetatable(this, mt) 43 | 44 | return this 45 | end 46 | 47 | function _M:set_url(page) 48 | local url = self.url 49 | 50 | if str_find(url, '[&%?]page=') then 51 | url = str_gsub(url, '([&%?])page=[^&#]*', '%1page=' .. page) 52 | 53 | else 54 | if not str_find(url, '%?') then 55 | url = url .. '?' 56 | end 57 | 58 | if str_find(url, '%w=') then 59 | url = url .. '&' 60 | end 61 | 62 | url = url .. 'page=' .. page 63 | end 64 | 65 | return url 66 | end 67 | 68 | function _M:prev() 69 | if self.page == 1 then 70 | return "prev" 71 | end 72 | 73 | return 'prev' 74 | end 75 | 76 | function _M:next() 77 | if self.page < self.page_count then 78 | return 'next' 79 | end 80 | 81 | return "next" 82 | end 83 | 84 | function _M:url_range(list, tlen, first, last) 85 | last = last or first 86 | 87 | for i = first, last do 88 | if i == self.page then 89 | tlen = tab_append(list, tlen, ' ', 90 | i, '') 91 | 92 | else 93 | tlen = tab_append(list, tlen, '', 94 | i, '') 95 | end 96 | end 97 | 98 | return tlen 99 | end 100 | 101 | function _M:slide(each_side) 102 | local ret = {} 103 | local tlen = 0 104 | 105 | each_side = each_side or 2 106 | local window = each_side * 2 107 | local left_dot_end 108 | local right_dot_begin 109 | 110 | if self.page > each_side + 3 then 111 | left_dot_end = self.page - each_side 112 | if left_dot_end > self.page_count - window then 113 | left_dot_end = self.page_count - window 114 | end 115 | if left_dot_end <= 3 then 116 | left_dot_end = nil 117 | end 118 | end 119 | 120 | if self.page <= self.page_count - (each_side + 3) 121 | and self.page_count > (each_side + 3) 122 | then 123 | right_dot_begin = self.page + each_side 124 | if right_dot_begin <= window then 125 | right_dot_begin = window + 1 126 | end 127 | if self.page_count - right_dot_begin <= 2 then 128 | right_dot_begin = nil 129 | end 130 | end 131 | 132 | if left_dot_end then 133 | tlen = self:url_range(ret, tlen, 1) 134 | tlen = tab_append(ret, tlen, " ... ") 135 | if right_dot_begin then 136 | tlen = self:url_range(ret, tlen, left_dot_end, right_dot_begin) 137 | tlen = tab_append(ret, tlen, " ... ") 138 | tlen = self:url_range(ret, tlen, self.page_count) 139 | 140 | else 141 | tlen = self:url_range(ret, tlen, left_dot_end, self.page_count) 142 | end 143 | 144 | else 145 | if right_dot_begin then 146 | tlen = self:url_range(ret, tlen, 1, right_dot_begin) 147 | tlen = tab_append(ret, tlen, " ... ") 148 | tlen = self:url_range(ret, tlen, self.page_count) 149 | 150 | else 151 | tlen = self:url_range(ret, tlen, 1, self.page_count) 152 | end 153 | end 154 | 155 | return tab_concat(ret) 156 | end 157 | 158 | function _M:show() 159 | local ret = '
' 160 | .. ' ' .. self:prev() 161 | .. ' ' .. self:slide() 162 | .. ' ' .. self:next() 163 | .. '
' 164 | 165 | return ret 166 | end 167 | 168 | function _M.paging(total_count, page_size, curr_page) 169 | total_count = total_count or 0 170 | curr_page = curr_page or 1 171 | if curr_page < 1 then 172 | curr_page = 1 173 | end 174 | page_size = page_size or 20 175 | 176 | local page_count, remainder = modf(total_count / page_size) 177 | if remainder > 0 then 178 | page_count = page_count + 1 179 | end 180 | if curr_page > page_count then 181 | curr_page = page_count 182 | end 183 | 184 | local offset = (curr_page - 1) * page_size 185 | 186 | local limit = page_size 187 | if limit > total_count then 188 | limit = total_count 189 | end 190 | 191 | return limit, offset 192 | end 193 | 194 | return _M 195 | -------------------------------------------------------------------------------- /web/lua/opmserver/templates.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This Lua code was generated by Lemplate, the Lua 3 | Template Toolkit. Any changes made to this file will be lost the next 4 | time the templates are compiled. 5 | 6 | Copyright 2016 - Yichun Zhang (agentzh) - All rights reserved. 7 | 8 | Copyright 2006-2014 - Ingy döt Net - All rights reserved. 9 | ]] 10 | 11 | local gsub = ngx.re.gsub 12 | local concat = table.concat 13 | local type = type 14 | local math_floor = math.floor 15 | local table_maxn = table.maxn 16 | 17 | local _M = { 18 | version = '0.07' 19 | } 20 | 21 | local template_map = {} 22 | 23 | local function tt2_true(v) 24 | return v and v ~= 0 and v ~= "" and v ~= '0' 25 | end 26 | 27 | local function tt2_not(v) 28 | return not v or v == 0 or v == "" or v == '0' 29 | end 30 | 31 | local context_meta = {} 32 | 33 | function context_meta.plugin(context, name, args) 34 | if name == "iterator" then 35 | local list = args[1] 36 | local count = table_maxn(list) 37 | return { list = list, count = 1, max = count - 1, index = 0, size = count, first = true, last = false, prev = "" } 38 | else 39 | return error("unknown iterator: " .. name) 40 | end 41 | end 42 | 43 | function context_meta.process(context, file) 44 | local f = template_map[file] 45 | if not f then 46 | return error("file error - " .. file .. ": not found") 47 | end 48 | return f(context) 49 | end 50 | 51 | function context_meta.include(context, file) 52 | local f = template_map[file] 53 | if not f then 54 | return error("file error - " .. file .. ": not found") 55 | end 56 | return f(context) 57 | end 58 | 59 | context_meta = { __index = context_meta } 60 | 61 | -- XXX debugging function: 62 | -- local function xxx(data) 63 | -- io.stderr:write("\n" .. require("cjson").encode(data) .. "\n") 64 | -- end 65 | 66 | local function stash_get(stash, expr) 67 | local result 68 | 69 | if type(expr) ~= "table" then 70 | result = stash[expr] 71 | if type(result) == "function" then 72 | return result() 73 | end 74 | return result or '' 75 | end 76 | 77 | result = stash 78 | for i = 1, #expr, 2 do 79 | local key = expr[i] 80 | if type(key) == "number" and key == math_floor(key) and key >= 0 then 81 | key = key + 1 82 | end 83 | local val = result[key] 84 | local args = expr[i + 1] 85 | if args == 0 then 86 | args = {} 87 | end 88 | 89 | if val == nil then 90 | if not _M.vmethods[key] then 91 | if type(expr[i + 1]) == "table" then 92 | return error("virtual method " .. key .. " not supported") 93 | end 94 | return '' 95 | end 96 | val = _M.vmethods[key] 97 | args = {result, unpack(args)} 98 | end 99 | 100 | if type(val) == "function" then 101 | val = val(unpack(args)) 102 | end 103 | 104 | result = val 105 | end 106 | 107 | return result 108 | end 109 | 110 | local function stash_set(stash, k, v, default) 111 | if default then 112 | local old = stash[k] 113 | if old == nil then 114 | stash[k] = v 115 | end 116 | else 117 | stash[k] = v 118 | end 119 | end 120 | 121 | _M.vmethods = { 122 | join = function (list, delim) 123 | delim = delim or ' ' 124 | local out = {} 125 | local size = #list 126 | for i = 1, size, 1 do 127 | out[i * 2 - 1] = list[i] 128 | if i ~= size then 129 | out[i * 2] = delim 130 | end 131 | end 132 | return concat(out) 133 | end, 134 | 135 | first = function (list) 136 | return list[1] 137 | end, 138 | 139 | keys = function (list) 140 | local out = {} 141 | i = 1 142 | for key in pairs(list) do 143 | out[i] = key 144 | i = i + 1 145 | end 146 | return out 147 | end, 148 | 149 | last = function (list) 150 | return list[#list] 151 | end, 152 | 153 | push = function(list, ...) 154 | local n = select("#", ...) 155 | local m = #list 156 | for i = 1, n do 157 | list[m + i] = select(i, ...) 158 | end 159 | return '' 160 | end, 161 | 162 | size = function (list) 163 | if type(list) == "table" then 164 | return #list 165 | else 166 | return 1 167 | end 168 | end, 169 | 170 | sort = function (list) 171 | local out = { unpack(list) } 172 | table.sort(out) 173 | return out 174 | end, 175 | 176 | split = function (str, delim) 177 | delim = delim or ' ' 178 | local out = {} 179 | local start = 1 180 | local sub = string.sub 181 | local find = string.find 182 | local sstart, send = find(str, delim, start) 183 | local i = 1 184 | while sstart do 185 | out[i] = sub(str, start, sstart-1) 186 | i = i + 1 187 | start = send + 1 188 | sstart, send = find(str, delim, start) 189 | end 190 | out[i] = sub(str, start) 191 | return out 192 | end, 193 | } 194 | 195 | _M.filters = { 196 | html = function (s, args) 197 | s = gsub(s, "&", '&', "jo") 198 | s = gsub(s, "<", '<', "jo"); 199 | s = gsub(s, ">", '>', "jo"); 200 | s = gsub(s, '"', '"', "jo"); -- " end quote for emacs 201 | return s 202 | end, 203 | 204 | lower = function (s, args) 205 | return string.lower(s) 206 | end, 207 | 208 | upper = function (s, args) 209 | return string.upper(s) 210 | end, 211 | } 212 | 213 | function _M.process(file, params) 214 | local stash = params 215 | local context = { 216 | stash = stash, 217 | filter = function (bits, name, params) 218 | local s = concat(bits) 219 | local f = _M.filters[name] 220 | if f then 221 | return f(s, params) 222 | end 223 | return error("filter '" .. name .. "' not found") 224 | end 225 | } 226 | context = setmetatable(context, context_meta) 227 | local f = template_map[file] 228 | if not f then 229 | return error("file error - " .. file .. ": not found") 230 | end 231 | return f(context) 232 | end 233 | -- 404.tt2 234 | template_map['404.tt2'] = function (context) 235 | if not context then 236 | return error("Lemplate function called without context\n") 237 | end 238 | local stash = context.stash 239 | local output = {} 240 | local i = 0 241 | 242 | i = i + 1 output[i] = '\n

\n404 not found!\n

\n' 243 | 244 | return output 245 | end 246 | 247 | -- analytics.tt2 248 | template_map['analytics.tt2'] = function (context) 249 | if not context then 250 | return error("Lemplate function called without context\n") 251 | end 252 | local stash = context.stash 253 | local output = {} 254 | local i = 0 255 | 256 | i = i + 1 output[i] = '\n\n' 257 | 258 | return output 259 | end 260 | 261 | -- docs.tt2 262 | template_map['docs.tt2'] = function (context) 263 | if not context then 264 | return error("Lemplate function called without context\n") 265 | end 266 | local stash = context.stash 267 | local output = {} 268 | local i = 0 269 | 270 | i = i + 1 output[i] = '\n
\n
\n

Docs

\n
\n\n' 271 | -- line 7 "docs.tt2" 272 | i = i + 1 output[i] = stash_get(stash, 'doc_html') 273 | i = i + 1 output[i] = '\n
\n' 274 | 275 | return output 276 | end 277 | 278 | -- error.tt2 279 | template_map['error.tt2'] = function (context) 280 | if not context then 281 | return error("Lemplate function called without context\n") 282 | end 283 | local stash = context.stash 284 | local output = {} 285 | local i = 0 286 | 287 | i = i + 1 output[i] = '\n
\n
\n

Error

\n
\n\n
\n ' 288 | -- line 8 "error.tt2" 289 | 290 | -- FILTER 291 | local value 292 | do 293 | local output = {} 294 | local i = 0 295 | 296 | i = i + 1 output[i] = stash_get(stash, 'error_info') 297 | 298 | value = context.filter(output, 'html', {}) 299 | end 300 | i = i + 1 output[i] = value 301 | 302 | i = i + 1 output[i] = '\n
\n\n
\n' 303 | 304 | return output 305 | end 306 | 307 | -- footer.tt2 308 | template_map['footer.tt2'] = function (context) 309 | if not context then 310 | return error("Lemplate function called without context\n") 311 | end 312 | local stash = context.stash 313 | local output = {} 314 | local i = 0 315 | 316 | i = i + 1 output[i] = '\n

' 317 | -- line 2 "footer.tt2" 318 | i = i + 1 output[i] = 'Copyright © 2016-2021 Yichun Zhang (agentzh)' 319 | i = i + 1 output[i] = '

\n

' 320 | -- line 3 "footer.tt2" 321 | i = i + 1 output[i] = '100% Powered by OpenResty and PostgreSQL' 322 | i = i + 1 output[i] = '\n ' 323 | -- line 4 "footer.tt2" 324 | i = i + 1 output[i] = '(' 325 | i = i + 1 output[i] = '' 326 | -- line 4 "footer.tt2" 327 | i = i + 1 output[i] = 'view the source code of this site' 328 | i = i + 1 output[i] = '' 329 | -- line 4 "footer.tt2" 330 | i = i + 1 output[i] = ')' 331 | i = i + 1 output[i] = '

\n

京ICP备16021991号

' 332 | 333 | return output 334 | end 335 | 336 | -- index.tt2 337 | template_map['index.tt2'] = function (context) 338 | if not context then 339 | return error("Lemplate function called without context\n") 340 | end 341 | local stash = context.stash 342 | local output = {} 343 | local i = 0 344 | 345 | i = i + 1 output[i] = '\n
\n
\n
\n

opm is the official OpenResty package manager, similar to Perl\'s CPAN and NodeJS\'s npm in rationale.

\n

We provide both the opm client-side command-line utility and the server-side application for the central package repository.

\n

Please read the opm documentation for more details.

\n

We already have ' 346 | -- line 8 "index.tt2" 347 | i = i + 1 output[i] = stash_get(stash, 'total_uploads') 348 | i = i + 1 output[i] = ' successful uploads across ' 349 | -- line 8 "index.tt2" 350 | i = i + 1 output[i] = stash_get(stash, 'package_count') 351 | i = i + 1 output[i] = ' distinct package names from ' 352 | -- line 8 "index.tt2" 353 | i = i + 1 output[i] = stash_get(stash, 'uploader_count') 354 | i = i + 1 output[i] = ' contributors. Come on, OPM authors!

\n
\n
\n
\n\n
\n
\n

Recent packages

\n \n (view all)\n \n \n Recent uploads\n \n
\n\n
\n' 355 | -- line 25 "index.tt2" 356 | i = i + 1 output[i] = context.process(context, 'package_list.tt2') 357 | i = i + 1 output[i] = '\n
\n
\n' 358 | 359 | return output 360 | end 361 | 362 | -- layout.tt2 363 | template_map['layout.tt2'] = function (context) 364 | if not context then 365 | return error("Lemplate function called without context\n") 366 | end 367 | local stash = context.stash 368 | local output = {} 369 | local i = 0 370 | 371 | i = i + 1 output[i] = '\n\n\n \n OPM - OpenResty Package Manager\n \n \n \n\n\n
\n
\n ' 372 | -- line 13 "layout.tt2" 373 | i = i + 1 output[i] = context.process(context, 'nav.tt2') 374 | i = i + 1 output[i] = '\n
\n\n
\n ' 375 | -- line 17 "layout.tt2" 376 | i = i + 1 output[i] = stash_get(stash, 'main_html') 377 | i = i + 1 output[i] = '\n
\n
\n\n \n\n' 381 | -- line 25 "layout.tt2" 382 | i = i + 1 output[i] = context.process(context, 'analytics.tt2') 383 | i = i + 1 output[i] = '\n\n\n' 384 | 385 | return output 386 | end 387 | 388 | -- login.tt2 389 | template_map['login.tt2'] = function (context) 390 | if not context then 391 | return error("Lemplate function called without context\n") 392 | end 393 | local stash = context.stash 394 | local output = {} 395 | local i = 0 396 | 397 | i = i + 1 output[i] = '\n
\n
\n

Sign In

\n
\n\n\n' 877 | 878 | return output 879 | end 880 | 881 | -- uploads.tt2 882 | template_map['uploads.tt2'] = function (context) 883 | if not context then 884 | return error("Lemplate function called without context\n") 885 | end 886 | local stash = context.stash 887 | local output = {} 888 | local i = 0 889 | 890 | i = i + 1 output[i] = '\n
\n\n\n' 891 | -- line 10 "uploads.tt2" 892 | i = i + 1 output[i] = context.process(context, 'package_list.tt2') 893 | i = i + 1 output[i] = '\n
\n' 894 | 895 | return output 896 | end 897 | 898 | return _M 899 | -------------------------------------------------------------------------------- /web/lua/opmserver/utils.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | 4 | local json_encode = require "cjson.safe" .encode 5 | local json_decode = require("cjson.safe").decode 6 | local resty_random = require("resty.random") 7 | local str = require("resty.string") 8 | local random_bytes = resty_random.bytes 9 | local to_hex = str.to_hex 10 | 11 | 12 | local ngx = ngx 13 | local ngx_exit = ngx.exit 14 | local read_body = ngx.req.read_body 15 | local get_body_data = ngx.req.get_body_data 16 | local re_find = ngx.re.find 17 | local re_gsub = ngx.re.gsub 18 | local io_open = io.open 19 | local type = type 20 | 21 | 22 | local HTML_ENTITIES = { 23 | ["&"] = "&", 24 | ["<"] = "<", 25 | [">"] = ">", 26 | ['"'] = """, 27 | ["'"] = "'", 28 | } 29 | 30 | local CODE_ENTITIES = { 31 | ["{"] = "{", 32 | ["}"] = "}", 33 | ["&"] = "&", 34 | ["<"] = "<", 35 | [">"] = ">", 36 | ['"'] = """, 37 | ["'"] = "'", 38 | } 39 | 40 | 41 | function _M.to_html(s, c) 42 | if type(s) == "string" then 43 | if c then 44 | s = re_gsub(s, "[}{\"><'&]", CODE_ENTITIES, "jo") 45 | else 46 | s = re_gsub(s, "[\"><'&]", HTML_ENTITIES, "jo") 47 | end 48 | end 49 | 50 | return s 51 | end 52 | 53 | 54 | function _M.read_file(path) 55 | local fp, err = io_open(path) 56 | if not fp then 57 | return nil, err 58 | end 59 | 60 | local chunk = fp:read("*all") 61 | fp:close() 62 | 63 | return chunk 64 | end 65 | 66 | 67 | function _M.gen_random_string(length) 68 | length = length or 32 69 | length = length / 2 70 | local r = random_bytes(length, true) 71 | 72 | return to_hex(r) 73 | end 74 | 75 | 76 | local function remove_null(t) 77 | for k, v in pairs(t) do 78 | if type(v) == "userdata" then 79 | t[k] = nil 80 | end 81 | 82 | if type(v) == "table" then 83 | remove_null(v) 84 | end 85 | end 86 | return t 87 | end 88 | 89 | 90 | function _M.get_post_args(explicit) 91 | read_body() 92 | 93 | local data = get_body_data() 94 | 95 | if not data and explicit then 96 | return ngx_exit(400) 97 | end 98 | 99 | if not data then 100 | return {} 101 | end 102 | 103 | if data and #data > 0 then 104 | local res, err = json_decode(data) 105 | if not res then 106 | return nil, "invalid json" 107 | end 108 | return remove_null(res) 109 | end 110 | 111 | return nil 112 | end 113 | 114 | 115 | local function _output(res) 116 | local data = json_encode(res) 117 | 118 | ngx.status = 200 119 | ngx.say(data) 120 | ngx.exit(200) 121 | end 122 | 123 | 124 | function _M.exit_ok(data) 125 | return _output({ status = 0, data = data }) 126 | end 127 | 128 | 129 | function _M.exit_err(msg) 130 | return _output({ status = 1, msg = msg }) 131 | end 132 | 133 | 134 | function _M.shell_escape(s, quote) 135 | local typ = type(s) 136 | if typ == "string" then 137 | if re_find(s, [=[[^a-zA-Z0-9._+:@%/-]]=]) then 138 | s = re_gsub(s, [[\\]], [[\\\\]], 'jo') 139 | s = re_gsub(s, [=[[ {}\[\]\(\)"'`#&,;<>\?\$\^\|!~]]=], [[\$0]], 'jo') 140 | end 141 | end 142 | 143 | if quote then 144 | s = quote .. s .. quote 145 | end 146 | 147 | return s 148 | end 149 | 150 | 151 | return _M 152 | -------------------------------------------------------------------------------- /web/lua/opmserver/view.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local templates = require "opmserver.templates" 4 | local get_current_user 5 | local say = ngx.say 6 | 7 | function _M.show(tpl_name, context) 8 | if not get_current_user then 9 | get_current_user = require "opmserver".get_current_user 10 | end 11 | 12 | context = context or {} 13 | 14 | local curr_user, err = get_current_user() 15 | if curr_user then 16 | context.curr_user = curr_user 17 | end 18 | 19 | tpl_name = tpl_name .. '.tt2' 20 | local sub_tpl_html = templates.process(tpl_name, context) 21 | context.main_html = sub_tpl_html 22 | 23 | local html = templates.process('layout.tt2', context) 24 | say(html) 25 | end 26 | 27 | return _M 28 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon.lua: -------------------------------------------------------------------------------- 1 | return require("pgmoon.init") 2 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon/arrays.lua: -------------------------------------------------------------------------------- 1 | local PostgresArray 2 | do 3 | local _class_0 4 | local _base_0 = { } 5 | _base_0.__index = _base_0 6 | _class_0 = setmetatable({ 7 | __init = function() end, 8 | __base = _base_0, 9 | __name = "PostgresArray" 10 | }, { 11 | __index = _base_0, 12 | __call = function(cls, ...) 13 | local _self_0 = setmetatable({}, _base_0) 14 | cls.__init(_self_0, ...) 15 | return _self_0 16 | end 17 | }) 18 | _base_0.__class = _class_0 19 | PostgresArray = _class_0 20 | end 21 | getmetatable(PostgresArray).__call = function(self, t) 22 | return setmetatable(t, self.__base) 23 | end 24 | local default_escape_literal = nil 25 | local encode_array 26 | do 27 | local append_buffer 28 | append_buffer = function(escape_literal, buffer, values) 29 | for _index_0 = 1, #values do 30 | local item = values[_index_0] 31 | if type(item) == "table" and not getmetatable(item) then 32 | table.insert(buffer, "[") 33 | append_buffer(escape_literal, buffer, item) 34 | buffer[#buffer] = "]" 35 | table.insert(buffer, ",") 36 | else 37 | table.insert(buffer, escape_literal(item)) 38 | table.insert(buffer, ",") 39 | end 40 | end 41 | return buffer 42 | end 43 | encode_array = function(tbl, escape_literal) 44 | escape_literal = escape_literal or default_escape_literal 45 | if not (escape_literal) then 46 | local Postgres 47 | Postgres = require("pgmoon").Postgres 48 | default_escape_literal = function(v) 49 | return Postgres.escape_literal(nil, v) 50 | end 51 | escape_literal = default_escape_literal 52 | end 53 | local buffer = append_buffer(escape_literal, { 54 | "ARRAY[" 55 | }, tbl) 56 | buffer[#buffer] = "]" 57 | return table.concat(buffer) 58 | end 59 | end 60 | local convert_values 61 | convert_values = function(array, fn) 62 | for idx, v in ipairs(array) do 63 | if type(v) == "table" then 64 | convert_values(v, fn) 65 | else 66 | array[idx] = fn(v) 67 | end 68 | end 69 | return array 70 | end 71 | local decode_array 72 | do 73 | local P, R, S, V, Ct, C, Cs 74 | do 75 | local _obj_0 = require("lpeg") 76 | P, R, S, V, Ct, C, Cs = _obj_0.P, _obj_0.R, _obj_0.S, _obj_0.V, _obj_0.Ct, _obj_0.C, _obj_0.Cs 77 | end 78 | local g = P({ 79 | "array", 80 | array = Ct(V("open") * (V("value") * (P(",") * V("value")) ^ 0) ^ -1 * V("close")), 81 | value = V("invalid_char") + V("string") + V("array") + V("literal"), 82 | string = P('"') * Cs((P([[\\]]) / [[\]] + P([[\"]]) / [["]] + (P(1) - P('"'))) ^ 0) * P('"'), 83 | literal = C((P(1) - S("},")) ^ 1), 84 | invalid_char = S(" \t\r\n") / function() 85 | return error("got unexpected whitespace") 86 | end, 87 | open = P("{"), 88 | delim = P(","), 89 | close = P("}") 90 | }) 91 | decode_array = function(str, convert_fn) 92 | local out = (assert(g:match(str), "failed to parse postgresql array")) 93 | setmetatable(out, PostgresArray.__base) 94 | if convert_fn then 95 | return convert_values(out, convert_fn) 96 | else 97 | return out 98 | end 99 | end 100 | end 101 | return { 102 | encode_array = encode_array, 103 | decode_array = decode_array, 104 | PostgresArray = PostgresArray 105 | } 106 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon/crypto.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return { 3 | md5 = ngx.md5 4 | } 5 | end 6 | local crypto = require("crypto") 7 | local md5 8 | md5 = function(str) 9 | return crypto.digest("md5", str) 10 | end 11 | return { 12 | md5 = md5 13 | } 14 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon/init.lua: -------------------------------------------------------------------------------- 1 | local socket = require("pgmoon.socket") 2 | local insert 3 | insert = table.insert 4 | local rshift, lshift, band 5 | do 6 | local _obj_0 = require("bit") 7 | rshift, lshift, band = _obj_0.rshift, _obj_0.lshift, _obj_0.band 8 | end 9 | local VERSION = "1.4.0" 10 | local _len 11 | _len = function(thing, t) 12 | if t == nil then 13 | t = type(thing) 14 | end 15 | local _exp_0 = t 16 | if "string" == _exp_0 then 17 | return #thing 18 | elseif "table" == _exp_0 then 19 | local l = 0 20 | for _index_0 = 1, #thing do 21 | local inner = thing[_index_0] 22 | local inner_t = type(inner) 23 | if inner_t == "string" then 24 | l = l + #inner 25 | else 26 | l = l + _len(inner, inner_t) 27 | end 28 | end 29 | return l 30 | else 31 | return error("don't know how to calculate length of " .. tostring(t)) 32 | end 33 | end 34 | local _debug_msg 35 | _debug_msg = function(str) 36 | return require("moon").dump((function() 37 | local _accum_0 = { } 38 | local _len_0 = 1 39 | for p in str:gmatch("[^%z]+") do 40 | _accum_0[_len_0] = p 41 | _len_0 = _len_0 + 1 42 | end 43 | return _accum_0 44 | end)()) 45 | end 46 | local flipped 47 | flipped = function(t) 48 | local keys 49 | do 50 | local _accum_0 = { } 51 | local _len_0 = 1 52 | for k in pairs(t) do 53 | _accum_0[_len_0] = k 54 | _len_0 = _len_0 + 1 55 | end 56 | keys = _accum_0 57 | end 58 | for _index_0 = 1, #keys do 59 | local key = keys[_index_0] 60 | t[t[key]] = key 61 | end 62 | return t 63 | end 64 | local MSG_TYPE = flipped({ 65 | status = "S", 66 | auth = "R", 67 | backend_key = "K", 68 | ready_for_query = "Z", 69 | query = "Q", 70 | notice = "N", 71 | password = "p", 72 | row_description = "T", 73 | data_row = "D", 74 | command_complete = "C", 75 | error = "E" 76 | }) 77 | local ERROR_TYPES = flipped({ 78 | severity = "S", 79 | code = "C", 80 | message = "M", 81 | position = "P", 82 | detail = "D" 83 | }) 84 | local PG_TYPES = { 85 | [16] = "boolean", 86 | [17] = "bytea", 87 | [20] = "number", 88 | [21] = "number", 89 | [23] = "number", 90 | [700] = "number", 91 | [701] = "number", 92 | [1700] = "number", 93 | [1000] = "array_boolean", 94 | [1005] = "array_number", 95 | [1007] = "array_number", 96 | [1016] = "array_number", 97 | [1021] = "array_number", 98 | [1022] = "array_number", 99 | [1231] = "array_number", 100 | [1009] = "array_string", 101 | [1015] = "array_string", 102 | [1002] = "array_string", 103 | [1014] = "array_string", 104 | [2951] = "array_string", 105 | [114] = "json", 106 | [3802] = "json" 107 | } 108 | local NULL = "\0" 109 | local tobool 110 | tobool = function(str) 111 | return str == "t" 112 | end 113 | local Postgres 114 | do 115 | local _class_0 116 | local _base_0 = { 117 | convert_null = false, 118 | NULL = { 119 | "NULL" 120 | }, 121 | user = "postgres", 122 | host = "127.0.0.1", 123 | port = "5432", 124 | type_deserializers = { 125 | json = function(self, val, name) 126 | local decode_json 127 | decode_json = require("pgmoon.json").decode_json 128 | return decode_json(val) 129 | end, 130 | bytea = function(self, val, name) 131 | return self:decode_bytea(val) 132 | end, 133 | array_boolean = function(self, val, name) 134 | local decode_array 135 | decode_array = require("pgmoon.arrays").decode_array 136 | return decode_array(val, tobool) 137 | end, 138 | array_number = function(self, val, name) 139 | local decode_array 140 | decode_array = require("pgmoon.arrays").decode_array 141 | return decode_array(val, tonumber) 142 | end, 143 | array_string = function(self, val, name) 144 | local decode_array 145 | decode_array = require("pgmoon.arrays").decode_array 146 | return decode_array(val) 147 | end 148 | }, 149 | connect = function(self) 150 | self.sock = socket.new() 151 | local pool_name = self.user .. ":" .. self.database 152 | .. ":" .. self.host .. ":" .. self.port 153 | 154 | local ok, err = self.sock:connect(self.host, self.port, 155 | { pool = pool_name }) 156 | if not (ok) then 157 | return nil, err 158 | end 159 | if self.sock:getreusedtimes() == 0 then 160 | local success 161 | success, err = self:send_startup_message() 162 | if not (success) then 163 | return nil, err 164 | end 165 | success, err = self:auth() 166 | if not (success) then 167 | return nil, err 168 | end 169 | success, err = self:wait_until_ready() 170 | if not (success) then 171 | return nil, err 172 | end 173 | end 174 | return true 175 | end, 176 | disconnect = function(self) 177 | local sock = self.sock 178 | self.sock = nil 179 | return sock:close() 180 | end, 181 | keepalive = function(self, ...) 182 | local sock = self.sock 183 | self.sock = nil 184 | return sock:setkeepalive(...) 185 | end, 186 | auth = function(self) 187 | local t, msg = self:receive_message() 188 | if not (t) then 189 | return nil, msg 190 | end 191 | if not (MSG_TYPE.auth == t) then 192 | self:disconnect() 193 | if MSG_TYPE.error == t then 194 | return nil, self:parse_error(msg) 195 | end 196 | error("unexpected message during auth: " .. tostring(t)) 197 | end 198 | local auth_type = self:decode_int(msg, 4) 199 | local _exp_0 = auth_type 200 | if 0 == _exp_0 then 201 | return true 202 | elseif 3 == _exp_0 then 203 | return self:cleartext_auth(msg) 204 | elseif 5 == _exp_0 then 205 | return self:md5_auth(msg) 206 | else 207 | return error("don't know how to auth: " .. tostring(auth_type)) 208 | end 209 | end, 210 | cleartext_auth = function(self, msg) 211 | assert(self.password, "missing password, required for connect") 212 | self:send_message(MSG_TYPE.password, { 213 | self.password, 214 | NULL 215 | }) 216 | return self:check_auth() 217 | end, 218 | md5_auth = function(self, msg) 219 | local md5 220 | md5 = require("pgmoon.crypto").md5 221 | local salt = msg:sub(5, 8) 222 | assert(self.password, "missing password, required for connect") 223 | self:send_message(MSG_TYPE.password, { 224 | "md5", 225 | md5(md5(self.password .. self.user) .. salt), 226 | NULL 227 | }) 228 | return self:check_auth() 229 | end, 230 | check_auth = function(self) 231 | local t, msg = self:receive_message() 232 | if not (t) then 233 | return nil, msg 234 | end 235 | local _exp_0 = t 236 | if MSG_TYPE.error == _exp_0 then 237 | return nil, self:parse_error(msg) 238 | elseif MSG_TYPE.auth == _exp_0 then 239 | return true 240 | else 241 | return error("unknown response from auth") 242 | end 243 | end, 244 | query = function(self, q) 245 | self:send_message(MSG_TYPE.query, { 246 | q, 247 | NULL 248 | }) 249 | local row_desc, data_rows, command_complete, err_msg 250 | local result 251 | local num_queries = 0 252 | while true do 253 | local t, msg = self:receive_message() 254 | if not (t) then 255 | return nil, msg 256 | end 257 | local _exp_0 = t 258 | if MSG_TYPE.data_row == _exp_0 then 259 | data_rows = data_rows or { } 260 | insert(data_rows, msg) 261 | elseif MSG_TYPE.row_description == _exp_0 then 262 | row_desc = msg 263 | elseif MSG_TYPE.error == _exp_0 then 264 | err_msg = msg 265 | elseif MSG_TYPE.command_complete == _exp_0 then 266 | command_complete = msg 267 | local next_result = self:format_query_result(row_desc, data_rows, command_complete) 268 | num_queries = num_queries + 1 269 | if num_queries == 1 then 270 | result = next_result 271 | elseif num_queries == 2 then 272 | result = { 273 | result, 274 | next_result 275 | } 276 | else 277 | insert(result, next_result) 278 | end 279 | row_desc, data_rows, command_complete = nil 280 | elseif MSG_TYPE.ready_for_query == _exp_0 then 281 | break 282 | end 283 | end 284 | if err_msg then 285 | return nil, self:parse_error(err_msg), result, num_queries 286 | end 287 | return result, num_queries 288 | end, 289 | format_query_result = function(self, row_desc, data_rows, command_complete) 290 | local command, affected_rows 291 | if command_complete then 292 | command = command_complete:match("^%w+") 293 | affected_rows = tonumber(command_complete:match("%d+%z$")) 294 | end 295 | if row_desc then 296 | if not (data_rows) then 297 | return { } 298 | end 299 | local fields = self:parse_row_desc(row_desc) 300 | local num_rows = #data_rows 301 | for i = 1, num_rows do 302 | data_rows[i] = self:parse_data_row(data_rows[i], fields) 303 | end 304 | if affected_rows and command ~= "SELECT" then 305 | data_rows.affected_rows = affected_rows 306 | end 307 | return data_rows 308 | end 309 | if affected_rows then 310 | return { 311 | affected_rows = affected_rows 312 | } 313 | else 314 | return true 315 | end 316 | end, 317 | parse_error = function(self, err_msg) 318 | local severity, message, detail, position 319 | local offset = 1 320 | while offset <= #err_msg do 321 | local t = err_msg:sub(offset, offset) 322 | local str = err_msg:match("[^%z]+", offset + 1) 323 | if not (str) then 324 | break 325 | end 326 | offset = offset + (2 + #str) 327 | local _exp_0 = t 328 | if ERROR_TYPES.severity == _exp_0 then 329 | severity = str 330 | elseif ERROR_TYPES.message == _exp_0 then 331 | message = str 332 | elseif ERROR_TYPES.position == _exp_0 then 333 | position = str 334 | elseif ERROR_TYPES.detail == _exp_0 then 335 | detail = str 336 | end 337 | end 338 | local msg = tostring(severity) .. ": " .. tostring(message) 339 | if position then 340 | msg = tostring(msg) .. " (" .. tostring(position) .. ")" 341 | end 342 | if detail then 343 | msg = tostring(msg) .. "\n" .. tostring(detail) 344 | end 345 | return msg 346 | end, 347 | parse_row_desc = function(self, row_desc) 348 | local num_fields = self:decode_int(row_desc:sub(1, 2)) 349 | local offset = 3 350 | local fields 351 | do 352 | local _accum_0 = { } 353 | local _len_0 = 1 354 | for i = 1, num_fields do 355 | local name = row_desc:match("[^%z]+", offset) 356 | offset = offset + #name + 1 357 | local data_type = self:decode_int(row_desc:sub(offset + 6, offset + 6 + 3)) 358 | data_type = PG_TYPES[data_type] or "string" 359 | local format = self:decode_int(row_desc:sub(offset + 16, offset + 16 + 1)) 360 | assert(0 == format, "don't know how to handle format") 361 | offset = offset + 18 362 | local _value_0 = { 363 | name, 364 | data_type 365 | } 366 | _accum_0[_len_0] = _value_0 367 | _len_0 = _len_0 + 1 368 | end 369 | fields = _accum_0 370 | end 371 | return fields 372 | end, 373 | parse_data_row = function(self, data_row, fields) 374 | local num_fields = self:decode_int(data_row:sub(1, 2)) 375 | local out = { } 376 | local offset = 3 377 | for i = 1, num_fields do 378 | local _continue_0 = false 379 | repeat 380 | local field = fields[i] 381 | if not (field) then 382 | _continue_0 = true 383 | break 384 | end 385 | local field_name, field_type 386 | field_name, field_type = field[1], field[2] 387 | local len = self:decode_int(data_row:sub(offset, offset + 3)) 388 | offset = offset + 4 389 | if len < 0 then 390 | if self.convert_null then 391 | out[field_name] = self.NULL 392 | end 393 | _continue_0 = true 394 | break 395 | end 396 | local value = data_row:sub(offset, offset + len - 1) 397 | offset = offset + len 398 | local _exp_0 = field_type 399 | if "number" == _exp_0 then 400 | value = tonumber(value) 401 | elseif "boolean" == _exp_0 then 402 | value = value == "t" 403 | elseif "string" == _exp_0 then 404 | local _ = nil 405 | else 406 | do 407 | local fn = self.type_deserializers[field_type] 408 | if fn then 409 | value = fn(self, value, field_type) 410 | end 411 | end 412 | end 413 | out[field_name] = value 414 | _continue_0 = true 415 | until true 416 | if not _continue_0 then 417 | break 418 | end 419 | end 420 | return out 421 | end, 422 | wait_until_ready = function(self) 423 | while true do 424 | local t, msg = self:receive_message() 425 | if not (t) then 426 | return nil, msg 427 | end 428 | if MSG_TYPE.error == t then 429 | self:disconnect() 430 | return nil, self:parse_error(msg) 431 | end 432 | if MSG_TYPE.ready_for_query == t then 433 | break 434 | end 435 | end 436 | return true 437 | end, 438 | receive_message = function(self) 439 | local t, err = self.sock:receive(1) 440 | if not (t) then 441 | self:disconnect() 442 | return nil, "receive_message: failed to get type: " .. tostring(err) 443 | end 444 | local len 445 | len, err = self.sock:receive(4) 446 | if not (len) then 447 | self:disconnect() 448 | return nil, "receive_message: failed to get len: " .. tostring(err) 449 | end 450 | len = self:decode_int(len) 451 | len = len - 4 452 | local msg = self.sock:receive(len) 453 | return t, msg 454 | end, 455 | send_startup_message = function(self) 456 | assert(self.user, "missing user for connect") 457 | assert(self.database, "missing database for connect") 458 | local data = { 459 | self:encode_int(196608), 460 | "user", 461 | NULL, 462 | self.user, 463 | NULL, 464 | "database", 465 | NULL, 466 | self.database, 467 | NULL, 468 | NULL 469 | } 470 | return self.sock:send({ 471 | self:encode_int(_len(data) + 4), 472 | data 473 | }) 474 | end, 475 | send_message = function(self, t, data, len) 476 | if len == nil then 477 | len = nil 478 | end 479 | if len == nil then 480 | len = _len(data) 481 | end 482 | len = len + 4 483 | return self.sock:send({ 484 | t, 485 | self:encode_int(len), 486 | data 487 | }) 488 | end, 489 | decode_int = function(self, str, bytes) 490 | if bytes == nil then 491 | bytes = #str 492 | end 493 | local _exp_0 = bytes 494 | if 4 == _exp_0 then 495 | local d, c, b, a = str:byte(1, 4) 496 | return a + lshift(b, 8) + lshift(c, 16) + lshift(d, 24) 497 | elseif 2 == _exp_0 then 498 | local b, a = str:byte(1, 2) 499 | return a + lshift(b, 8) 500 | else 501 | return error("don't know how to decode " .. tostring(bytes) .. " byte(s)") 502 | end 503 | end, 504 | encode_int = function(self, n, bytes) 505 | if bytes == nil then 506 | bytes = 4 507 | end 508 | local _exp_0 = bytes 509 | if 4 == _exp_0 then 510 | local a = band(n, 0xff) 511 | local b = band(rshift(n, 8), 0xff) 512 | local c = band(rshift(n, 16), 0xff) 513 | local d = band(rshift(n, 24), 0xff) 514 | return string.char(d, c, b, a) 515 | else 516 | return error("don't know how to encode " .. tostring(bytes) .. " byte(s)") 517 | end 518 | end, 519 | decode_bytea = function(self, str) 520 | if str:sub(1, 2) == '\\x' then 521 | return str:sub(3):gsub('..', function(hex) 522 | return string.char(tonumber(hex, 16)) 523 | end) 524 | else 525 | return str:gsub('\\(%d%d%d)', function(oct) 526 | return string.char(tonumber(oct, 8)) 527 | end) 528 | end 529 | end, 530 | encode_bytea = function(self, str) 531 | return string.format("E'\\\\x%s'", str:gsub('.', function(byte) 532 | return string.format('%02x', string.byte(byte)) 533 | end)) 534 | end, 535 | escape_identifier = function(self, ident) 536 | return '"' .. (tostring(ident):gsub('"', '""')) .. '"' 537 | end, 538 | escape_literal = function(self, val) 539 | local _exp_0 = type(val) 540 | if "number" == _exp_0 then 541 | return tostring(val) 542 | elseif "string" == _exp_0 then 543 | return "'" .. tostring((val:gsub("'", "''"))) .. "'" 544 | elseif "boolean" == _exp_0 then 545 | return val and "TRUE" or "FALSE" 546 | end 547 | return error("don't know how to escape value: " .. tostring(val)) 548 | end, 549 | __tostring = function(self) 550 | return "" 551 | end 552 | } 553 | _base_0.__index = _base_0 554 | _class_0 = setmetatable({ 555 | __init = function(self, opts) 556 | if opts then 557 | self.user = opts.user 558 | self.host = opts.host 559 | self.database = opts.database 560 | self.port = opts.port 561 | self.password = opts.password 562 | end 563 | end, 564 | __base = _base_0, 565 | __name = "Postgres" 566 | }, { 567 | __index = _base_0, 568 | __call = function(cls, ...) 569 | local _self_0 = setmetatable({}, _base_0) 570 | cls.__init(_self_0, ...) 571 | return _self_0 572 | end 573 | }) 574 | _base_0.__class = _class_0 575 | Postgres = _class_0 576 | end 577 | return { 578 | Postgres = Postgres, 579 | new = Postgres, 580 | VERSION = VERSION 581 | } 582 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon/json.lua: -------------------------------------------------------------------------------- 1 | local default_escape_literal = nil 2 | local encode_json 3 | encode_json = function(tbl, escape_literal) 4 | escape_literal = escape_literal or default_escape_literal 5 | local json = require("cjson") 6 | if not (escape_literal) then 7 | local Postgres 8 | Postgres = require("pgmoon").Postgres 9 | default_escape_literal = function(v) 10 | return Postgres.escape_literal(nil, v) 11 | end 12 | escape_literal = default_escape_literal 13 | end 14 | local enc = json.encode(tbl) 15 | return escape_literal(enc) 16 | end 17 | local decode_json 18 | decode_json = function(str) 19 | local json = require("cjson") 20 | return json.decode(str) 21 | end 22 | return { 23 | encode_json = encode_json, 24 | decode_json = decode_json 25 | } 26 | -------------------------------------------------------------------------------- /web/lua/vendor/pgmoon/socket.lua: -------------------------------------------------------------------------------- 1 | local luasocket 2 | do 3 | local __flatten 4 | __flatten = function(t, buffer) 5 | local _exp_0 = type(t) 6 | if "string" == _exp_0 then 7 | buffer[#buffer + 1] = t 8 | elseif "table" == _exp_0 then 9 | for _index_0 = 1, #t do 10 | local thing = t[_index_0] 11 | __flatten(thing, buffer) 12 | end 13 | end 14 | end 15 | local _flatten 16 | _flatten = function(t) 17 | local buffer = { } 18 | __flatten(t, buffer) 19 | return table.concat(buffer) 20 | end 21 | local proxy_mt = { 22 | __index = function(self, key) 23 | local sock = self.sock 24 | local original = sock[key] 25 | if type(original) == "function" then 26 | local fn 27 | fn = function(_, ...) 28 | return original(sock, ...) 29 | end 30 | self[key] = fn 31 | return fn 32 | else 33 | return original 34 | end 35 | end 36 | } 37 | luasocket = { 38 | tcp = function(...) 39 | local socket = require("socket") 40 | local sock = socket.tcp(...) 41 | local proxy = setmetatable({ 42 | sock = sock, 43 | send = function(self, ...) 44 | return self.sock:send(_flatten(...)) 45 | end, 46 | getreusedtimes = function(self) 47 | return 0 48 | end 49 | }, proxy_mt) 50 | return proxy 51 | end 52 | } 53 | end 54 | return { 55 | new = function() 56 | if ngx and ngx.get_phase() ~= "init" then 57 | return ngx.socket.tcp() 58 | else 59 | return luasocket.tcp() 60 | end 61 | end 62 | } 63 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/cookie.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2013-2016 Jiale Zhi (calio), CloudFlare Inc. 2 | -- See RFC6265 http://tools.ietf.org/search/rfc6265 3 | -- require "luacov" 4 | 5 | local type = type 6 | local byte = string.byte 7 | local sub = string.sub 8 | local format = string.format 9 | local log = ngx.log 10 | local ERR = ngx.ERR 11 | local WARN = ngx.WARN 12 | local ngx_header = ngx.header 13 | 14 | local EQUAL = byte("=") 15 | local SEMICOLON = byte(";") 16 | local SPACE = byte(" ") 17 | local HTAB = byte("\t") 18 | 19 | -- table.new(narr, nrec) 20 | local ok, new_tab = pcall(require, "table.new") 21 | if not ok then 22 | new_tab = function () return {} end 23 | end 24 | 25 | local ok, clear_tab = pcall(require, "table.clear") 26 | if not ok then 27 | clear_tab = function(tab) for k, _ in pairs(tab) do tab[k] = nil end end 28 | end 29 | 30 | local _M = new_tab(0, 2) 31 | 32 | _M._VERSION = '0.01' 33 | 34 | 35 | local function get_cookie_table(text_cookie) 36 | if type(text_cookie) ~= "string" then 37 | log(ERR, format("expect text_cookie to be \"string\" but found %s", 38 | type(text_cookie))) 39 | return {} 40 | end 41 | 42 | local EXPECT_KEY = 1 43 | local EXPECT_VALUE = 2 44 | local EXPECT_SP = 3 45 | 46 | local n = 0 47 | local len = #text_cookie 48 | 49 | for i=1, len do 50 | if byte(text_cookie, i) == SEMICOLON then 51 | n = n + 1 52 | end 53 | end 54 | 55 | local cookie_table = new_tab(0, n + 1) 56 | 57 | local state = EXPECT_SP 58 | local i = 1 59 | local j = 1 60 | local key, value 61 | 62 | while j <= len do 63 | if state == EXPECT_KEY then 64 | if byte(text_cookie, j) == EQUAL then 65 | key = sub(text_cookie, i, j - 1) 66 | state = EXPECT_VALUE 67 | i = j + 1 68 | end 69 | elseif state == EXPECT_VALUE then 70 | if byte(text_cookie, j) == SEMICOLON 71 | or byte(text_cookie, j) == SPACE 72 | or byte(text_cookie, j) == HTAB 73 | then 74 | value = sub(text_cookie, i, j - 1) 75 | cookie_table[key] = value 76 | 77 | key, value = nil, nil 78 | state = EXPECT_SP 79 | i = j + 1 80 | end 81 | elseif state == EXPECT_SP then 82 | if byte(text_cookie, j) ~= SPACE 83 | and byte(text_cookie, j) ~= HTAB 84 | then 85 | state = EXPECT_KEY 86 | i = j 87 | j = j - 1 88 | end 89 | end 90 | j = j + 1 91 | end 92 | 93 | if key ~= nil and value == nil then 94 | cookie_table[key] = sub(text_cookie, i) 95 | end 96 | 97 | return cookie_table 98 | end 99 | 100 | function _M.new(self) 101 | local _cookie = ngx.var.http_cookie 102 | --if not _cookie then 103 | --return nil, "no cookie found in current request" 104 | --end 105 | return setmetatable({ _cookie = _cookie, set_cookie_table = new_tab(4, 0) }, 106 | { __index = self }) 107 | end 108 | 109 | function _M.get(self, key) 110 | if not self._cookie then 111 | return nil, "no cookie found in the current request" 112 | end 113 | if self.cookie_table == nil then 114 | self.cookie_table = get_cookie_table(self._cookie) 115 | end 116 | 117 | return self.cookie_table[key] 118 | end 119 | 120 | function _M.get_all(self) 121 | if not self._cookie then 122 | return nil, "no cookie found in the current request" 123 | end 124 | 125 | if self.cookie_table == nil then 126 | self.cookie_table = get_cookie_table(self._cookie) 127 | end 128 | 129 | return self.cookie_table 130 | end 131 | 132 | local function bake(cookie) 133 | if not cookie.key or not cookie.value then 134 | return nil, 'missing cookie field "key" or "value"' 135 | end 136 | 137 | if cookie["max-age"] then 138 | cookie.max_age = cookie["max-age"] 139 | end 140 | 141 | if (cookie.samesite) then 142 | local samesite = cookie.samesite 143 | 144 | -- if we dont have a valid-looking attribute, ignore the attribute 145 | if (samesite ~= "Strict" and samesite ~= "Lax" and samesite ~= "None") then 146 | log(WARN, "SameSite value must be 'Strict', 'Lax' or 'None'") 147 | cookie.samesite = nil 148 | end 149 | end 150 | 151 | local str = cookie.key .. "=" .. cookie.value 152 | .. (cookie.expires and "; Expires=" .. cookie.expires or "") 153 | .. (cookie.max_age and "; Max-Age=" .. cookie.max_age or "") 154 | .. (cookie.domain and "; Domain=" .. cookie.domain or "") 155 | .. (cookie.path and "; Path=" .. cookie.path or "") 156 | .. (cookie.secure and "; Secure" or "") 157 | .. (cookie.httponly and "; HttpOnly" or "") 158 | .. (cookie.samesite and "; SameSite=" .. cookie.samesite or "") 159 | .. (cookie.extension and "; " .. cookie.extension or "") 160 | return str 161 | end 162 | 163 | function _M.set(self, cookie) 164 | local cookie_str, err = bake(cookie) 165 | if not cookie_str then 166 | return nil, err 167 | end 168 | 169 | local set_cookie = ngx_header['Set-Cookie'] 170 | local set_cookie_type = type(set_cookie) 171 | local t = self.set_cookie_table 172 | clear_tab(t) 173 | 174 | if set_cookie_type == "string" then 175 | -- only one cookie has been setted 176 | if set_cookie ~= cookie_str then 177 | t[1] = set_cookie 178 | t[2] = cookie_str 179 | ngx_header['Set-Cookie'] = t 180 | end 181 | elseif set_cookie_type == "table" then 182 | -- more than one cookies has been setted 183 | local size = #set_cookie 184 | 185 | -- we can not set cookie like ngx.header['Set-Cookie'][3] = val 186 | -- so create a new table, copy all the values, and then set it back 187 | for i=1, size do 188 | t[i] = ngx_header['Set-Cookie'][i] 189 | if t[i] == cookie_str then 190 | -- new cookie is duplicated 191 | return true 192 | end 193 | end 194 | t[size + 1] = cookie_str 195 | ngx_header['Set-Cookie'] = t 196 | else 197 | -- no cookie has been setted 198 | ngx_header['Set-Cookie'] = cookie_str 199 | end 200 | return true 201 | end 202 | 203 | return _M 204 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/http.lua: -------------------------------------------------------------------------------- 1 | local http_headers = require "resty.http_headers" 2 | 3 | local ngx_socket_tcp = ngx.socket.tcp 4 | local ngx_req = ngx.req 5 | local ngx_req_socket = ngx_req.socket 6 | local ngx_req_get_headers = ngx_req.get_headers 7 | local ngx_req_get_method = ngx_req.get_method 8 | local str_gmatch = string.gmatch 9 | local str_lower = string.lower 10 | local str_upper = string.upper 11 | local str_find = string.find 12 | local str_sub = string.sub 13 | local str_gsub = string.gsub 14 | local tbl_concat = table.concat 15 | local tbl_insert = table.insert 16 | local ngx_encode_args = ngx.encode_args 17 | local ngx_re_match = ngx.re.match 18 | local ngx_re_gsub = ngx.re.gsub 19 | local ngx_log = ngx.log 20 | local ngx_DEBUG = ngx.DEBUG 21 | local ngx_ERR = ngx.ERR 22 | local ngx_NOTICE = ngx.NOTICE 23 | local ngx_var = ngx.var 24 | local co_yield = coroutine.yield 25 | local co_create = coroutine.create 26 | local co_status = coroutine.status 27 | local co_resume = coroutine.resume 28 | 29 | 30 | -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 31 | local HOP_BY_HOP_HEADERS = { 32 | ["connection"] = true, 33 | ["keep-alive"] = true, 34 | ["proxy-authenticate"] = true, 35 | ["proxy-authorization"] = true, 36 | ["te"] = true, 37 | ["trailers"] = true, 38 | ["transfer-encoding"] = true, 39 | ["upgrade"] = true, 40 | ["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal 41 | -- with this (may send chunked for example). 42 | } 43 | 44 | 45 | -- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot 46 | -- be resumed. This protects user code from inifite loops when doing things like 47 | -- repeat 48 | -- local chunk, err = res.body_reader() 49 | -- if chunk then -- <-- This could be a string msg in the core wrap function. 50 | -- ... 51 | -- end 52 | -- until not chunk 53 | local co_wrap = function(func) 54 | local co = co_create(func) 55 | if not co then 56 | return nil, "could not create coroutine" 57 | else 58 | return function(...) 59 | if co_status(co) == "suspended" then 60 | return select(2, co_resume(co, ...)) 61 | else 62 | return nil, "can't resume a " .. co_status(co) .. " coroutine" 63 | end 64 | end 65 | end 66 | end 67 | 68 | 69 | local _M = { 70 | _VERSION = '0.09', 71 | } 72 | _M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version 73 | 74 | local mt = { __index = _M } 75 | 76 | 77 | local HTTP = { 78 | [1.0] = " HTTP/1.0\r\n", 79 | [1.1] = " HTTP/1.1\r\n", 80 | } 81 | 82 | local DEFAULT_PARAMS = { 83 | method = "GET", 84 | path = "/", 85 | version = 1.1, 86 | } 87 | 88 | 89 | function _M.new(self) 90 | local sock, err = ngx_socket_tcp() 91 | if not sock then 92 | return nil, err 93 | end 94 | return setmetatable({ sock = sock, keepalive = true }, mt) 95 | end 96 | 97 | 98 | function _M.set_timeout(self, timeout) 99 | local sock = self.sock 100 | if not sock then 101 | return nil, "not initialized" 102 | end 103 | 104 | return sock:settimeout(timeout) 105 | end 106 | 107 | 108 | function _M.ssl_handshake(self, ...) 109 | local sock = self.sock 110 | if not sock then 111 | return nil, "not initialized" 112 | end 113 | 114 | self.ssl = true 115 | 116 | return sock:sslhandshake(...) 117 | end 118 | 119 | 120 | function _M.connect(self, ...) 121 | local sock = self.sock 122 | if not sock then 123 | return nil, "not initialized" 124 | end 125 | 126 | self.host = select(1, ...) 127 | self.port = select(2, ...) 128 | 129 | -- If port is not a number, this is likely a unix domain socket connection. 130 | if type(self.port) ~= "number" then 131 | self.port = nil 132 | end 133 | 134 | self.keepalive = true 135 | 136 | return sock:connect(...) 137 | end 138 | 139 | 140 | function _M.set_keepalive(self, ...) 141 | local sock = self.sock 142 | if not sock then 143 | return nil, "not initialized" 144 | end 145 | 146 | if self.keepalive == true then 147 | return sock:setkeepalive(...) 148 | else 149 | -- The server said we must close the connection, so we cannot setkeepalive. 150 | -- If close() succeeds we return 2 instead of 1, to differentiate between 151 | -- a normal setkeepalive() failure and an intentional close(). 152 | local res, err = sock:close() 153 | if res then 154 | return 2, "connection must be closed" 155 | else 156 | return res, err 157 | end 158 | end 159 | end 160 | 161 | 162 | function _M.get_reused_times(self) 163 | local sock = self.sock 164 | if not sock then 165 | return nil, "not initialized" 166 | end 167 | 168 | return sock:getreusedtimes() 169 | end 170 | 171 | 172 | function _M.close(self) 173 | local sock = self.sock 174 | if not sock then 175 | return nil, "not initialized" 176 | end 177 | 178 | return sock:close() 179 | end 180 | 181 | 182 | local function _should_receive_body(method, code) 183 | if method == "HEAD" then return nil end 184 | if code == 204 or code == 304 then return nil end 185 | if code >= 100 and code < 200 then return nil end 186 | return true 187 | end 188 | 189 | 190 | function _M.parse_uri(self, uri) 191 | local m, err = ngx_re_match(uri, [[^(http[s]?)://([^:/]+)(?::(\d+))?(.*)]], 192 | "jo") 193 | 194 | if not m then 195 | if err then 196 | return nil, "failed to match the uri: " .. uri .. ", " .. err 197 | end 198 | 199 | return nil, "bad uri: " .. uri 200 | else 201 | if m[3] then 202 | m[3] = tonumber(m[3]) 203 | else 204 | if m[1] == "https" then 205 | m[3] = 443 206 | else 207 | m[3] = 80 208 | end 209 | end 210 | if not m[4] or "" == m[4] then m[4] = "/" end 211 | return m, nil 212 | end 213 | end 214 | 215 | 216 | local function _format_request(params) 217 | local version = params.version 218 | local headers = params.headers or {} 219 | 220 | local query = params.query or "" 221 | if query then 222 | if type(query) == "table" then 223 | query = "?" .. ngx_encode_args(query) 224 | end 225 | end 226 | 227 | -- Initialize request 228 | local req = { 229 | str_upper(params.method), 230 | " ", 231 | params.path, 232 | query, 233 | HTTP[version], 234 | -- Pre-allocate slots for minimum headers and carriage return. 235 | true, 236 | true, 237 | true, 238 | } 239 | local c = 6 -- req table index it's faster to do this inline vs table.insert 240 | 241 | -- Append headers 242 | for key, values in pairs(headers) do 243 | if type(values) ~= "table" then 244 | values = {values} 245 | end 246 | 247 | key = tostring(key) 248 | for _, value in pairs(values) do 249 | req[c] = key .. ": " .. tostring(value) .. "\r\n" 250 | c = c + 1 251 | end 252 | end 253 | 254 | -- Close headers 255 | req[c] = "\r\n" 256 | 257 | return tbl_concat(req) 258 | end 259 | 260 | 261 | local function _receive_status(sock) 262 | local line, err = sock:receive("*l") 263 | if not line then 264 | return nil, nil, nil, err 265 | end 266 | 267 | return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8)), str_sub(line, 14) 268 | end 269 | 270 | 271 | 272 | local function _receive_headers(sock) 273 | local headers = http_headers.new() 274 | 275 | repeat 276 | local line, err = sock:receive("*l") 277 | if not line then 278 | return nil, err 279 | end 280 | 281 | for key, val in str_gmatch(line, "([^:%s]+):%s*(.+)") do 282 | if headers[key] then 283 | if type(headers[key]) ~= "table" then 284 | headers[key] = { headers[key] } 285 | end 286 | tbl_insert(headers[key], tostring(val)) 287 | else 288 | headers[key] = tostring(val) 289 | end 290 | end 291 | until str_find(line, "^%s*$") 292 | 293 | return headers, nil 294 | end 295 | 296 | 297 | local function _chunked_body_reader(sock, default_chunk_size) 298 | return co_wrap(function(max_chunk_size) 299 | local max_chunk_size = max_chunk_size or default_chunk_size 300 | local remaining = 0 301 | local length 302 | 303 | repeat 304 | -- If we still have data on this chunk 305 | if max_chunk_size and remaining > 0 then 306 | 307 | if remaining > max_chunk_size then 308 | -- Consume up to max_chunk_size 309 | length = max_chunk_size 310 | remaining = remaining - max_chunk_size 311 | else 312 | -- Consume all remaining 313 | length = remaining 314 | remaining = 0 315 | end 316 | else -- This is a fresh chunk 317 | 318 | -- Receive the chunk size 319 | local str, err = sock:receive("*l") 320 | if not str then 321 | co_yield(nil, err) 322 | end 323 | 324 | length = tonumber(str, 16) 325 | 326 | if not length then 327 | co_yield(nil, "unable to read chunksize") 328 | end 329 | 330 | if max_chunk_size and length > max_chunk_size then 331 | -- Consume up to max_chunk_size 332 | remaining = length - max_chunk_size 333 | length = max_chunk_size 334 | end 335 | end 336 | 337 | if length > 0 then 338 | local str, err = sock:receive(length) 339 | if not str then 340 | co_yield(nil, err) 341 | end 342 | 343 | max_chunk_size = co_yield(str) or default_chunk_size 344 | 345 | -- If we're finished with this chunk, read the carriage return. 346 | if remaining == 0 then 347 | sock:receive(2) -- read \r\n 348 | end 349 | else 350 | -- Read the last (zero length) chunk's carriage return 351 | sock:receive(2) -- read \r\n 352 | end 353 | 354 | until length == 0 355 | end) 356 | end 357 | 358 | 359 | local function _body_reader(sock, content_length, default_chunk_size) 360 | return co_wrap(function(max_chunk_size) 361 | local max_chunk_size = max_chunk_size or default_chunk_size 362 | 363 | if not content_length and max_chunk_size then 364 | -- We have no length, but wish to stream. 365 | -- HTTP 1.0 with no length will close connection, so read chunks to the end. 366 | repeat 367 | local str, err, partial = sock:receive(max_chunk_size) 368 | if not str and err == "closed" then 369 | max_chunk_size = tonumber(co_yield(partial, err) or default_chunk_size) 370 | end 371 | 372 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 373 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 374 | 375 | if not max_chunk_size then 376 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 377 | break 378 | end 379 | until not str 380 | 381 | elseif not content_length then 382 | -- We have no length but don't wish to stream. 383 | -- HTTP 1.0 with no length will close connection, so read to the end. 384 | co_yield(sock:receive("*a")) 385 | 386 | elseif not max_chunk_size then 387 | -- We have a length and potentially keep-alive, but want everything. 388 | co_yield(sock:receive(content_length)) 389 | 390 | else 391 | -- We have a length and potentially a keep-alive, and wish to stream 392 | -- the response. 393 | local received = 0 394 | repeat 395 | local length = max_chunk_size 396 | if received + length > content_length then 397 | length = content_length - received 398 | end 399 | 400 | if length > 0 then 401 | local str, err = sock:receive(length) 402 | if not str then 403 | max_chunk_size = tonumber(co_yield(nil, err) or default_chunk_size) 404 | end 405 | received = received + length 406 | 407 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 408 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 409 | 410 | if not max_chunk_size then 411 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 412 | break 413 | end 414 | end 415 | 416 | until length == 0 417 | end 418 | end) 419 | end 420 | 421 | 422 | local function _no_body_reader() 423 | return nil 424 | end 425 | 426 | 427 | local function _read_body(res) 428 | local reader = res.body_reader 429 | 430 | if not reader then 431 | -- Most likely HEAD or 304 etc. 432 | return nil, "no body to be read" 433 | end 434 | 435 | local chunks = {} 436 | local c = 1 437 | 438 | local chunk, err 439 | repeat 440 | chunk, err = reader() 441 | 442 | if err then 443 | return nil, err, tbl_concat(chunks) -- Return any data so far. 444 | end 445 | if chunk then 446 | chunks[c] = chunk 447 | c = c + 1 448 | end 449 | until not chunk 450 | 451 | return tbl_concat(chunks) 452 | end 453 | 454 | 455 | local function _trailer_reader(sock) 456 | return co_wrap(function() 457 | co_yield(_receive_headers(sock)) 458 | end) 459 | end 460 | 461 | 462 | local function _read_trailers(res) 463 | local reader = res.trailer_reader 464 | if not reader then 465 | return nil, "no trailers" 466 | end 467 | 468 | local trailers = reader() 469 | setmetatable(res.headers, { __index = trailers }) 470 | end 471 | 472 | 473 | local function _send_body(sock, body) 474 | if type(body) == 'function' then 475 | repeat 476 | local chunk, err, partial = body() 477 | 478 | if chunk then 479 | local ok,err = sock:send(chunk) 480 | 481 | if not ok then 482 | return nil, err 483 | end 484 | elseif err ~= nil then 485 | return nil, err, partial 486 | end 487 | 488 | until chunk == nil 489 | elseif body ~= nil then 490 | local bytes, err = sock:send(body) 491 | 492 | if not bytes then 493 | return nil, err 494 | end 495 | end 496 | return true, nil 497 | end 498 | 499 | 500 | local function _handle_continue(sock, body) 501 | local status, version, reason, err = _receive_status(sock) 502 | if not status then 503 | return nil, nil, err 504 | end 505 | 506 | -- Only send body if we receive a 100 Continue 507 | if status == 100 then 508 | local ok, err = sock:receive("*l") -- Read carriage return 509 | if not ok then 510 | return nil, nil, err 511 | end 512 | _send_body(sock, body) 513 | end 514 | return status, version, err 515 | end 516 | 517 | 518 | function _M.send_request(self, params) 519 | -- Apply defaults 520 | setmetatable(params, { __index = DEFAULT_PARAMS }) 521 | 522 | local sock = self.sock 523 | local body = params.body 524 | local headers = http_headers.new() 525 | 526 | local params_headers = params.headers 527 | if params_headers then 528 | -- We assign one by one so that the metatable can handle case insensitivity 529 | -- for us. You can blame the spec for this inefficiency. 530 | for k,v in pairs(params_headers) do 531 | headers[k] = v 532 | end 533 | end 534 | 535 | -- Ensure minimal headers are set 536 | if type(body) == 'string' and not headers["Content-Length"] then 537 | headers["Content-Length"] = #body 538 | end 539 | if not headers["Host"] then 540 | if (str_sub(self.host, 1, 5) == "unix:") then 541 | return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one." 542 | end 543 | -- If we have a port (i.e. not connected to a unix domain socket), and this 544 | -- port is non-standard, append it to the Host heaer. 545 | if self.port then 546 | if self.ssl and self.port ~= 443 then 547 | headers["Host"] = self.host .. ":" .. self.port 548 | elseif not self.ssl and self.port ~= 80 then 549 | headers["Host"] = self.host .. ":" .. self.port 550 | else 551 | headers["Host"] = self.host 552 | end 553 | else 554 | headers["Host"] = self.host 555 | end 556 | end 557 | if not headers["User-Agent"] then 558 | headers["User-Agent"] = _M._USER_AGENT 559 | end 560 | if params.version == 1.0 and not headers["Connection"] then 561 | headers["Connection"] = "Keep-Alive" 562 | end 563 | 564 | params.headers = headers 565 | 566 | -- Format and send request 567 | local req = _format_request(params) 568 | ngx_log(ngx_DEBUG, "\n", req) 569 | local bytes, err = sock:send(req) 570 | 571 | if not bytes then 572 | return nil, err 573 | end 574 | 575 | -- Send the request body, unless we expect: continue, in which case 576 | -- we handle this as part of reading the response. 577 | if headers["Expect"] ~= "100-continue" then 578 | local ok, err, partial = _send_body(sock, body) 579 | if not ok then 580 | return nil, err, partial 581 | end 582 | end 583 | 584 | return true 585 | end 586 | 587 | 588 | function _M.read_response(self, params) 589 | local sock = self.sock 590 | 591 | local status, version, reason, err 592 | 593 | -- If we expect: continue, we need to handle this, sending the body if allowed. 594 | -- If we don't get 100 back, then status is the actual status. 595 | if params.headers["Expect"] == "100-continue" then 596 | local _status, _version, _err = _handle_continue(sock, params.body) 597 | if not _status then 598 | return nil, _err 599 | elseif _status ~= 100 then 600 | status, version, err = _status, _version, _err 601 | end 602 | end 603 | 604 | -- Just read the status as normal. 605 | if not status then 606 | status, version, reason, err = _receive_status(sock) 607 | if not status then 608 | return nil, err 609 | end 610 | end 611 | 612 | 613 | local res_headers, err = _receive_headers(sock) 614 | if not res_headers then 615 | return nil, err 616 | end 617 | 618 | -- keepalive is true by default. Determine if this is correct or not. 619 | local ok, connection = pcall(str_lower, res_headers["Connection"]) 620 | if ok then 621 | if (version == 1.1 and connection == "close") or 622 | (version == 1.0 and connection ~= "keep-alive") then 623 | self.keepalive = false 624 | end 625 | else 626 | -- no connection header 627 | if version == 1.0 then 628 | self.keepalive = false 629 | end 630 | end 631 | 632 | local body_reader = _no_body_reader 633 | local trailer_reader, err = nil, nil 634 | local has_body = false 635 | 636 | -- Receive the body_reader 637 | if _should_receive_body(params.method, status) then 638 | local ok, encoding = pcall(str_lower, res_headers["Transfer-Encoding"]) 639 | if ok and version == 1.1 and encoding == "chunked" then 640 | body_reader, err = _chunked_body_reader(sock) 641 | has_body = true 642 | else 643 | 644 | local ok, length = pcall(tonumber, res_headers["Content-Length"]) 645 | if ok then 646 | body_reader, err = _body_reader(sock, length) 647 | has_body = true 648 | end 649 | end 650 | end 651 | 652 | if res_headers["Trailer"] then 653 | trailer_reader, err = _trailer_reader(sock) 654 | end 655 | 656 | if err then 657 | return nil, err 658 | else 659 | return { 660 | status = status, 661 | reason = reason, 662 | headers = res_headers, 663 | has_body = has_body, 664 | body_reader = body_reader, 665 | read_body = _read_body, 666 | trailer_reader = trailer_reader, 667 | read_trailers = _read_trailers, 668 | } 669 | end 670 | end 671 | 672 | 673 | function _M.request(self, params) 674 | local res, err = self:send_request(params) 675 | if not res then 676 | return res, err 677 | else 678 | return self:read_response(params) 679 | end 680 | end 681 | 682 | 683 | function _M.request_pipeline(self, requests) 684 | for i, params in ipairs(requests) do 685 | if params.headers and params.headers["Expect"] == "100-continue" then 686 | return nil, "Cannot pipeline request specifying Expect: 100-continue" 687 | end 688 | 689 | local res, err = self:send_request(params) 690 | if not res then 691 | return res, err 692 | end 693 | end 694 | 695 | local responses = {} 696 | for i, params in ipairs(requests) do 697 | responses[i] = setmetatable({ 698 | params = params, 699 | response_read = false, 700 | }, { 701 | -- Read each actual response lazily, at the point the user tries 702 | -- to access any of the fields. 703 | __index = function(t, k) 704 | local res, err 705 | if t.response_read == false then 706 | res, err = _M.read_response(self, t.params) 707 | t.response_read = true 708 | 709 | if not res then 710 | ngx_log(ngx_ERR, err) 711 | else 712 | for rk, rv in pairs(res) do 713 | t[rk] = rv 714 | end 715 | end 716 | end 717 | return rawget(t, k) 718 | end, 719 | }) 720 | end 721 | return responses 722 | end 723 | 724 | 725 | function _M.request_uri(self, uri, params) 726 | if not params then params = {} end 727 | 728 | local parsed_uri, err = self:parse_uri(uri) 729 | if not parsed_uri then 730 | return nil, err 731 | end 732 | 733 | local scheme, host, port, path = unpack(parsed_uri) 734 | if not params.path then params.path = path end 735 | 736 | local c, err = self:connect(host, port) 737 | if not c then 738 | return nil, err 739 | end 740 | 741 | if scheme == "https" then 742 | local verify = true 743 | if params.ssl_verify == false then 744 | verify = false 745 | end 746 | local ok, err = self:ssl_handshake(nil, host, verify) 747 | if not ok then 748 | return nil, err 749 | end 750 | end 751 | 752 | local res, err = self:request(params) 753 | if not res then 754 | return nil, err 755 | end 756 | 757 | local body, err = res:read_body() 758 | if not body then 759 | return nil, err 760 | end 761 | 762 | res.body = body 763 | 764 | local ok, err = self:set_keepalive() 765 | if not ok then 766 | ngx_log(ngx_ERR, err) 767 | end 768 | 769 | return res, nil 770 | end 771 | 772 | 773 | function _M.get_client_body_reader(self, chunksize, sock) 774 | local chunksize = chunksize or 65536 775 | if not sock then 776 | local ok, err 777 | ok, sock, err = pcall(ngx_req_socket) 778 | 779 | if not ok then 780 | return nil, sock -- pcall err 781 | end 782 | 783 | if not sock then 784 | if err == "no body" then 785 | return nil 786 | else 787 | return nil, err 788 | end 789 | end 790 | end 791 | 792 | local headers = ngx_req_get_headers() 793 | local length = headers.content_length 794 | local encoding = headers.transfer_encoding 795 | if length then 796 | return _body_reader(sock, tonumber(length), chunksize) 797 | elseif encoding and str_lower(encoding) == 'chunked' then 798 | -- Not yet supported by ngx_lua but should just work... 799 | return _chunked_body_reader(sock, chunksize) 800 | else 801 | return nil 802 | end 803 | end 804 | 805 | 806 | function _M.proxy_request(self, chunksize) 807 | return self:request{ 808 | method = ngx_req_get_method(), 809 | path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""), 810 | body = self:get_client_body_reader(chunksize), 811 | headers = ngx_req_get_headers(), 812 | } 813 | end 814 | 815 | 816 | function _M.proxy_response(self, response, chunksize) 817 | if not response then 818 | ngx_log(ngx_ERR, "no response provided") 819 | return 820 | end 821 | 822 | ngx.status = response.status 823 | 824 | -- Filter out hop-by-hop headeres 825 | for k,v in pairs(response.headers) do 826 | if not HOP_BY_HOP_HEADERS[str_lower(k)] then 827 | ngx.header[k] = v 828 | end 829 | end 830 | 831 | local reader = response.body_reader 832 | repeat 833 | local chunk, err = reader(chunksize) 834 | if err then 835 | ngx_log(ngx_ERR, err) 836 | break 837 | end 838 | 839 | if chunk then 840 | local res, err = ngx.print(chunk) 841 | if not res then 842 | ngx_log(ngx_ERR, err) 843 | break 844 | end 845 | end 846 | until not chunk 847 | end 848 | 849 | 850 | return _M 851 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/http_headers.lua: -------------------------------------------------------------------------------- 1 | local rawget, rawset, setmetatable = 2 | rawget, rawset, setmetatable 3 | 4 | local str_gsub = string.gsub 5 | local str_lower = string.lower 6 | 7 | 8 | local _M = { 9 | _VERSION = '0.01', 10 | } 11 | 12 | 13 | -- Returns an empty headers table with internalised case normalisation. 14 | -- Supports the same cases as in ngx_lua: 15 | -- 16 | -- headers.content_length 17 | -- headers["content-length"] 18 | -- headers["Content-Length"] 19 | function _M.new(self) 20 | local mt = { 21 | normalised = {}, 22 | } 23 | 24 | 25 | mt.__index = function(t, k) 26 | local k_hyphened = str_gsub(k, "_", "-") 27 | local matched = rawget(t, k) 28 | if matched then 29 | return matched 30 | else 31 | local k_normalised = str_lower(k_hyphened) 32 | return rawget(t, mt.normalised[k_normalised]) 33 | end 34 | end 35 | 36 | 37 | -- First check the normalised table. If there's no match (first time) add an entry for 38 | -- our current case in the normalised table. This is to preserve the human (prettier) case 39 | -- instead of outputting lowercased header names. 40 | -- 41 | -- If there's a match, we're being updated, just with a different case for the key. We use 42 | -- the normalised table to give us the original key, and perorm a rawset(). 43 | mt.__newindex = function(t, k, v) 44 | -- we support underscore syntax, so always hyphenate. 45 | local k_hyphened = str_gsub(k, "_", "-") 46 | 47 | -- lowercase hyphenated is "normalised" 48 | local k_normalised = str_lower(k_hyphened) 49 | 50 | if not mt.normalised[k_normalised] then 51 | mt.normalised[k_normalised] = k_hyphened 52 | rawset(t, k_hyphened, v) 53 | else 54 | rawset(t, mt.normalised[k_normalised], v) 55 | end 56 | end 57 | 58 | return setmetatable({}, mt) 59 | end 60 | 61 | 62 | return _M 63 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/ini.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Dejiang Zhu(doujiang24) 2 | 3 | 4 | local io_open = io.open 5 | local tonumber = tonumber 6 | local re_match = ngx.re.match 7 | local substr = string.sub 8 | 9 | 10 | local _M = { _VERSION = "0.01" } 11 | 12 | local section_pattern = [[ \A \[ ([^ \[ \] ]+) \] \z ]] 13 | local keyvalue_pattern = [[ \A ( [\w_]+ ) \s* = \s* ( ' [^']* ' | " [^"]* " | \S+ ) (?:\s*)? \z ]] 14 | 15 | 16 | function _M.parse_file(filename) 17 | local fp, err = io_open(filename) 18 | if not fp then 19 | return nil, "failed to open file: " .. (err or "") 20 | end 21 | 22 | local data = {}; 23 | local section 24 | 25 | for line in fp:lines() do 26 | local m = re_match(line, section_pattern, "jox") 27 | if m then 28 | section = m[1] 29 | 30 | else 31 | local m = re_match(line, keyvalue_pattern, "jox") 32 | if m then 33 | if not section then 34 | fp:close() 35 | return nil, "no section found before key value pairs appeared" 36 | end 37 | 38 | if not data[section] then 39 | data[section] = {} 40 | end 41 | 42 | local key, value = m[1], m[2] 43 | 44 | local val = tonumber(value) 45 | if val then 46 | -- do nothing 47 | 48 | elseif value == "true" then 49 | val = true 50 | 51 | elseif value == "false" then 52 | val = false 53 | 54 | elseif substr(value, 1, 1) == '"' or substr(value, 1, 1) == "'" then 55 | val = substr(value, 2, -2) 56 | 57 | else 58 | val = value 59 | end 60 | 61 | data[section][key] = val; 62 | end 63 | end 64 | end 65 | 66 | fp:close() 67 | 68 | return data 69 | end 70 | 71 | 72 | return _M 73 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/limit/req.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Yichun Zhang (agentzh) 2 | -- 3 | -- This library is an approximate Lua port of the standard ngx_limit_req 4 | -- module. 5 | 6 | 7 | local ffi = require "ffi" 8 | local math = require "math" 9 | 10 | 11 | local ngx_shared = ngx.shared 12 | local ngx_now = ngx.now 13 | local setmetatable = setmetatable 14 | local ffi_cast = ffi.cast 15 | local ffi_str = ffi.string 16 | local abs = math.abs 17 | local tonumber = tonumber 18 | local type = type 19 | local assert = assert 20 | 21 | 22 | -- TODO: we could avoid the tricky FFI cdata when lua_shared_dict supports 23 | -- hash-typed values as in redis. 24 | ffi.cdef[[ 25 | struct lua_resty_limit_req_rec { 26 | unsigned long excess; 27 | uint64_t last; /* time in milliseconds */ 28 | /* integer value, 1 corresponds to 0.001 r/s */ 29 | }; 30 | ]] 31 | local const_rec_ptr_type = ffi.typeof("const struct lua_resty_limit_req_rec*") 32 | local rec_size = ffi.sizeof("struct lua_resty_limit_req_rec") 33 | 34 | -- we can share the cdata here since we only need it temporarily for 35 | -- serialization inside the shared dict: 36 | local rec_cdata = ffi.new("struct lua_resty_limit_req_rec") 37 | 38 | 39 | local _M = { 40 | _VERSION = '0.01' 41 | } 42 | 43 | 44 | local mt = { 45 | __index = _M 46 | } 47 | 48 | 49 | function _M.new(dict_name, rate, burst) 50 | local dict = ngx_shared[dict_name] 51 | if not dict then 52 | return nil, "shared dict not found" 53 | end 54 | 55 | assert(rate > 0 and burst >= 0) 56 | 57 | local self = { 58 | dict = dict, 59 | rate = rate * 1000, 60 | burst = burst * 1000, 61 | } 62 | 63 | return setmetatable(self, mt) 64 | end 65 | 66 | 67 | -- sees an new incoming event 68 | -- the "commit" argument controls whether should we record the event in shm. 69 | -- FIXME we have a (small) race-condition window between dict:get() and 70 | -- dict:set() across multiple nginx worker processes. The size of the 71 | -- window is proportional to the number of workers. 72 | function _M.incoming(self, key, commit) 73 | local dict = self.dict 74 | local rate = self.rate 75 | local now = ngx_now() * 1000 76 | 77 | local excess 78 | 79 | -- it's important to anchor the string value for the read-only pointer 80 | -- cdata: 81 | local v = dict:get(key) 82 | if v then 83 | if type(v) ~= "string" or #v ~= rec_size then 84 | return nil, "shdict abused by other users" 85 | end 86 | local rec = ffi_cast(const_rec_ptr_type, v) 87 | local elapsed = now - tonumber(rec.last) 88 | 89 | -- print("elapsed: ", elapsed, "ms") 90 | 91 | -- we do not handle changing rate values specifically. the excess value 92 | -- can get automatically adjusted by the following formula with new rate 93 | -- values rather quickly anyway. 94 | excess = tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000 95 | 96 | if excess < 0 then 97 | -- ngx.log(ngx.WARN, "excess: ", excess / 1000) 98 | excess = 0 99 | end 100 | 101 | -- print("excess: ", excess) 102 | 103 | if excess > self.burst then 104 | return nil, "rejected" 105 | end 106 | 107 | else 108 | excess = 0 109 | end 110 | 111 | if commit then 112 | rec_cdata.excess = excess 113 | rec_cdata.last = now 114 | dict:set(key, ffi_str(rec_cdata, rec_size)) 115 | end 116 | 117 | -- return the delay in seconds, as well as excess 118 | return excess / rate, excess / 1000 119 | end 120 | 121 | 122 | function _M.uncommit(self, key) 123 | assert(key) 124 | local dict = self.dict 125 | 126 | local v = dict:get(key) 127 | if not v then 128 | return nil, "not found" 129 | end 130 | 131 | if type(v) ~= "string" or #v ~= rec_size then 132 | return nil, "shdict abused by other users" 133 | end 134 | 135 | local rec = ffi_cast(const_rec_ptr_type, v) 136 | 137 | local excess = tonumber(rec.excess) - 1000 138 | if excess < 0 then 139 | excess = 0 140 | end 141 | 142 | rec_cdata.excess = excess 143 | rec_cdata.last = rec.last 144 | dict:set(key, ffi_str(rec_cdata, rec_size)) 145 | return true 146 | end 147 | 148 | 149 | function _M.set_rate(self, rate) 150 | self.rate = rate * 1000 151 | end 152 | 153 | 154 | function _M.set_burst(self, burst) 155 | self.burst = burst * 1000 156 | end 157 | 158 | 159 | return _M 160 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/shell.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | version = 0.03 3 | } 4 | 5 | 6 | local resty_sig = require "resty.signal" 7 | local ngx_pipe = require "ngx.pipe" 8 | local new_tab = require "table.new" 9 | local tablepool = require "tablepool" 10 | 11 | 12 | local kill = resty_sig.kill 13 | local pipe_spawn = ngx_pipe.spawn 14 | local tostring = tostring 15 | local spawn_thread = ngx.thread.spawn 16 | local wait_thread = ngx.thread.wait 17 | local concat = table.concat 18 | local fetch_tab = tablepool.fetch 19 | local release_tab = tablepool.release 20 | local sleep = ngx.sleep 21 | 22 | 23 | local spawn_opts = { 24 | buffer_size = 1024 * 32 -- 32K 25 | } 26 | 27 | 28 | local tab_pool_tag = "resty.shell" 29 | 30 | 31 | local function cleanup_proc(proc) 32 | local pid = proc:pid() 33 | if pid then 34 | local ok, err = kill(pid, "TERM") 35 | if not ok then 36 | return nil, "failed to kill process " .. pid 37 | .. ": " .. tostring(err) 38 | end 39 | sleep(0.001) -- only wait for 1 msec 40 | kill(pid, "KILL") 41 | end 42 | 43 | return true 44 | end 45 | 46 | 47 | local function concat_err(err1, err2) 48 | return tostring(err1) .. "; " .. tostring(err2) 49 | end 50 | 51 | 52 | local function read_stream(proc, buf, max_size, meth_name) 53 | local pos = 1 54 | local len = 0 55 | 56 | while len <= max_size do 57 | local data, err, partial = proc[meth_name](proc, max_size - len + 1) 58 | if not data then 59 | if partial then 60 | buf[pos] = partial 61 | pos = pos + 1 62 | len = len + #partial 63 | end 64 | 65 | if err == "closed" then 66 | return pos - 1 67 | end 68 | 69 | return pos - 1, err 70 | end 71 | 72 | buf[pos] = data 73 | pos = pos + 1 74 | len = len + #data 75 | end 76 | 77 | if len > max_size then 78 | return pos - 1, "too much data" 79 | end 80 | 81 | return pos - 1 82 | end 83 | 84 | 85 | function _M.run(cmd, stdin, timeout, max_size) 86 | if not max_size then 87 | max_size = 128 * 1024 -- 128KB 88 | end 89 | 90 | local proc, err = pipe_spawn(cmd, spawn_opts) 91 | if not proc then 92 | return nil, nil, nil, "failed to spawn: " .. tostring(err) 93 | end 94 | 95 | proc:set_timeouts(timeout, timeout, timeout, timeout) 96 | 97 | if stdin and stdin ~= "" then 98 | local bytes, err = proc:write(stdin) 99 | if not bytes then 100 | local ok2, err2 = cleanup_proc(proc) 101 | if not ok2 then 102 | err = concat_err(err, err2) 103 | end 104 | return nil, nil, nil, "failed to write to stdin: " .. tostring(err) 105 | end 106 | end 107 | 108 | local ok 109 | ok, err = proc:shutdown("stdin") 110 | if not ok then 111 | local ok2, err2 = cleanup_proc(proc) 112 | if not ok2 then 113 | err = concat_err(err, err2) 114 | end 115 | return nil, nil, nil, "failed to shutdown stdin: " .. tostring(err) 116 | end 117 | 118 | local stdout_tab = fetch_tab(tab_pool_tag, 4, 0) 119 | local stderr_tab = fetch_tab(tab_pool_tag, 4, 0) 120 | 121 | local thr_out = spawn_thread(read_stream, proc, stdout_tab, max_size, 122 | "stdout_read_any") 123 | local thr_err = spawn_thread(read_stream, proc, stderr_tab, max_size, 124 | "stderr_read_any") 125 | 126 | local reason, status 127 | ok, reason, status = proc:wait() 128 | 129 | if ok == nil and reason ~= "exited" then 130 | err = reason 131 | local ok2, err2 = cleanup_proc(proc) 132 | if not ok2 then 133 | err = concat_err(err, err2) 134 | end 135 | 136 | local stdout = concat(stdout_tab) 137 | release_tab(tab_pool_tag, stdout_tab) 138 | 139 | local stderr = concat(stderr_tab) 140 | release_tab(tab_pool_tag, stderr_tab) 141 | 142 | return nil, stdout, stderr, 143 | "failed to wait for process: " .. tostring(err) 144 | end 145 | 146 | local ok2, stdout_pos, err2 = wait_thread(thr_out) 147 | if not ok2 then 148 | local stdout = concat(stdout_tab) 149 | release_tab(tab_pool_tag, stdout_tab) 150 | 151 | local stderr = concat(stderr_tab) 152 | release_tab(tab_pool_tag, stderr_tab) 153 | 154 | return nil, stdout, stderr, "failed to wait stdout thread: " 155 | .. tostring(stdout_pos) 156 | end 157 | 158 | if err2 then 159 | local stdout = concat(stdout_tab, "", 1, stdout_pos) 160 | release_tab(tab_pool_tag, stdout_tab) 161 | 162 | local stderr = concat(stderr_tab) 163 | release_tab(tab_pool_tag, stderr_tab) 164 | 165 | return nil, stdout, stderr, "failed to read stdout: " .. tostring(err2) 166 | end 167 | 168 | local stderr_pos 169 | ok2, stderr_pos, err2 = wait_thread(thr_err) 170 | if not ok2 then 171 | local stdout = concat(stdout_tab, "", 1, stdout_pos) 172 | release_tab(tab_pool_tag, stdout_tab) 173 | 174 | local stderr = concat(stderr_tab) 175 | release_tab(tab_pool_tag, stderr_tab) 176 | 177 | return nil, stdout, stderr, "failed to wait stderr thread: " 178 | .. tostring(stderr_pos) 179 | end 180 | 181 | local stdout = concat(stdout_tab, "", 1, stdout_pos) 182 | release_tab(tab_pool_tag, stdout_tab) 183 | 184 | local stderr = concat(stderr_tab, "", 1, stderr_pos) 185 | release_tab(tab_pool_tag, stderr_tab) 186 | 187 | if err2 then 188 | return nil, stdout, stderr, "failed to read stderr: " .. tostring(err2) 189 | end 190 | 191 | return ok, stdout, stderr, reason, status 192 | end 193 | 194 | 195 | return _M 196 | -------------------------------------------------------------------------------- /web/lua/vendor/resty/signal.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | version = 0.03 3 | } 4 | 5 | 6 | local ffi = require "ffi" 7 | local base = require "resty.core.base" 8 | 9 | 10 | local C = ffi.C 11 | local ffi_str = ffi.string 12 | local tonumber = tonumber 13 | local assert = assert 14 | local errno = ffi.errno 15 | local type = type 16 | local new_tab = base.new_tab 17 | local error = error 18 | local string_format = string.format 19 | 20 | 21 | local load_shared_lib 22 | do 23 | local string_gmatch = string.gmatch 24 | local string_match = string.match 25 | local io_open = io.open 26 | local io_close = io.close 27 | 28 | local cpath = package.cpath 29 | 30 | function load_shared_lib(so_name) 31 | local tried_paths = new_tab(32, 0) 32 | local i = 1 33 | 34 | for k, _ in string_gmatch(cpath, "[^;]+") do 35 | local fpath = string_match(k, "(.*/)") 36 | fpath = fpath .. so_name 37 | -- Don't get me wrong, the only way to know if a file exist is 38 | -- trying to open it. 39 | local f = io_open(fpath) 40 | if f ~= nil then 41 | io_close(f) 42 | return ffi.load(fpath) 43 | end 44 | 45 | tried_paths[i] = fpath 46 | i = i + 1 47 | end 48 | 49 | return nil, tried_paths 50 | end -- function 51 | end -- do 52 | 53 | 54 | local resty_signal, tried_paths = load_shared_lib("librestysignal.so") 55 | if not resty_signal then 56 | error("could not load librestysignal.so from the following paths:\n" .. 57 | table.concat(tried_paths, "\n"), 2) 58 | end 59 | 60 | 61 | ffi.cdef[[ 62 | int resty_signal_signum(int num); 63 | ]] 64 | 65 | 66 | if not pcall(function () return C.kill end) then 67 | ffi.cdef("int kill(int32_t pid, int sig);") 68 | end 69 | 70 | 71 | if not pcall(function () return C.strerror end) then 72 | ffi.cdef("char *strerror(int errnum);") 73 | end 74 | 75 | 76 | -- Below is just the ID numbers for each POSIX signal. We map these signal IDs 77 | -- to system-specific signal numbers on the C land (via librestysignal.so). 78 | local signals = { 79 | NONE = 0, 80 | HUP = 1, 81 | INT = 2, 82 | QUIT = 3, 83 | ILL = 4, 84 | TRAP = 5, 85 | ABRT = 6, 86 | BUS = 7, 87 | FPE = 8, 88 | KILL = 9, 89 | USR1 = 10, 90 | SEGV = 11, 91 | USR2 = 12, 92 | PIPE = 13, 93 | ALRM = 14, 94 | TERM = 15, 95 | CHLD = 17, 96 | CONT = 18, 97 | STOP = 19, 98 | TSTP = 20, 99 | TTIN = 21, 100 | TTOU = 22, 101 | URG = 23, 102 | XCPU = 24, 103 | XFSZ = 25, 104 | VTALRM = 26, 105 | PROF = 27, 106 | WINCH = 28, 107 | IO = 29, 108 | PWR = 30, 109 | EMT = 31, 110 | SYS = 32, 111 | INFO = 33 112 | } 113 | 114 | 115 | local function signum(name) 116 | local sig_num 117 | if type(name) == "number" then 118 | sig_num = name 119 | else 120 | local id = signals[name] 121 | if not id then 122 | return nil, "unknown signal name" 123 | end 124 | 125 | sig_num = tonumber(resty_signal.resty_signal_signum(id)) 126 | if sig_num < 0 then 127 | error( 128 | string_format("missing C def for signal %s = %d", name, id), 129 | 2 130 | ) 131 | end 132 | end 133 | return sig_num 134 | end 135 | 136 | 137 | function _M.kill(pid, sig) 138 | assert(sig) 139 | 140 | local sig_num, err = signum(sig) 141 | if err then 142 | return nil, err 143 | end 144 | 145 | local rc = tonumber(C.kill(assert(pid), sig_num)) 146 | if rc == 0 then 147 | return true 148 | end 149 | 150 | local err = ffi_str(C.strerror(errno())) 151 | return nil, err 152 | end 153 | 154 | _M.signum = signum 155 | 156 | return _M 157 | -------------------------------------------------------------------------------- /web/templates/404.tt2: -------------------------------------------------------------------------------- 1 | 2 |

3 | 404 not found! 4 |

5 | -------------------------------------------------------------------------------- /web/templates/analytics.tt2: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /web/templates/docs.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Docs

5 |
6 | 7 | [% doc_html %] 8 |
9 | -------------------------------------------------------------------------------- /web/templates/error.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Error

5 |
6 | 7 |
8 | [% error_info | html %] 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /web/templates/footer.tt2: -------------------------------------------------------------------------------- 1 | 2 |

[% "Copyright © 2016-2021 Yichun Zhang (agentzh)" %]

3 |

[% "100% Powered by OpenResty and PostgreSQL" %] 4 | [% "(" %][% "view the source code of this site" %][% ")" %]

5 |

京ICP备16021991号

-------------------------------------------------------------------------------- /web/templates/index.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

opm is the official OpenResty package manager, similar to Perl's CPAN and NodeJS's npm in rationale.

6 |

We provide both the opm client-side command-line utility and the server-side application for the central package repository.

7 |

Please read the opm documentation for more details.

8 |

We already have [% total_uploads %] successful uploads across [% package_count %] distinct package names from [% uploader_count %] contributors. Come on, OPM authors!

9 |
10 |
11 |
12 | 13 |
14 |
15 |

Recent packages

16 | 17 | (view all) 18 | 19 | 20 | Recent uploads 21 | 22 |
23 | 24 |
25 | [% PROCESS "package_list.tt2" %] 26 |
27 |
28 | -------------------------------------------------------------------------------- /web/templates/layout.tt2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OPM - OpenResty Package Manager 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | [% PROCESS "nav.tt2" %] 14 |
15 | 16 |
17 | [% main_html %] 18 |
19 |
20 | 21 | 24 | 25 | [% PROCESS "analytics.tt2" %] 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/templates/login.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Sign In

5 |
6 | 7 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /web/templates/nav.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 11 | 12 | 15 | 24 |
25 | -------------------------------------------------------------------------------- /web/templates/package_info.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

[% pkg_name | html %] 5 |

6 |
7 |

8 | [% pkg_info.abstract | html %] 9 |

10 |
11 |
12 | 13 | 23 | 24 |
25 |
$ opm get [% account %]/[% pkg_name | html %]
26 |
27 | 28 |
29 | [% pkg_doc %] 30 |
31 | 32 |

Authors

33 |
34 |

35 | [% pkg_info.authors | html %] 36 |

37 |
38 | 39 |

License

40 |
41 |

42 | [% pkg_info.licenses | html %] 43 |

44 |
45 | 46 | [%- IF pkg_info.dep_info %] 47 |

Dependencies

48 |
49 |

50 | [% pkg_info.dep_info %] 51 |

52 |
53 | [%- END %] 54 | 55 |

Versions

56 | 57 |
58 | [% PROCESS "package_list.tt2" %] 59 |
60 |
61 | -------------------------------------------------------------------------------- /web/templates/package_list.tt2: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | [%- FOREACH row IN packages %] 5 |
  • 6 | 7 | [% pkg_uploader_name = row.uploader_name; 8 | org = row.org_name; 9 | account = pkg_uploader_name; 10 | IF org; 11 | account = org; 12 | END %] 13 | 14 |
    15 | [%- IF row.raw_package_name %] 16 | 17 | [%- ELSE %] 18 | 19 | [%- END %] 20 | 21 | [% account _ "/" _ row.package_name %] 22 | 23 | 24 | [% row.version_s | html %] 25 | 26 | [%- IF uploader_page and curr_user and uploader_name and uploader_name == curr_user.login %] 27 | 28 | [%- IF not row.is_deleted %] 29 | 30 | [%- END %] 31 | [%- IF row.is_deleted %] 32 | pending deleting 33 | 34 | [%- END %] 35 | 36 | [%- END %] 37 | 38 | [%- IF row.indexed %] 39 | 40 | [%- ELSIF row.failed %] 41 | Failed 42 | [%- ELSE %] 43 | Pending 44 | [%- END %] 45 | 46 | by 47 | 48 | [% pkg_uploader_name %] 49 | 50 | 51 |
    52 |
    53 | [% row.abstract %] 54 | 55 | [% row.upload_updated_at | html %] 56 | 57 |
    58 |
  • 59 | [%- END %] 60 |
61 | 62 | [% page_info %] 63 | -------------------------------------------------------------------------------- /web/templates/packages.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Recent packages 5 | 6 | Recent uploads 7 | 8 |

9 |
10 | 11 | [% PROCESS "package_list.tt2" %] 12 |
13 | -------------------------------------------------------------------------------- /web/templates/search.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Search results

5 |
6 | 7 | [% PROCESS "package_list.tt2" %] 8 |
9 | -------------------------------------------------------------------------------- /web/templates/success.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Success

5 |
6 | 7 |
8 | [% success_info | html %] 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /web/templates/uploader.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

[% uploader_name | html %] 5 |

6 | 7 | GitHub 8 | 9 |
10 | 11 | 28 | 29 |

Packages

30 | 31 |
32 | [% PROCESS "package_list.tt2" %] 33 |
34 | 35 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /web/templates/uploads.tt2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Recent uploads

5 | 6 | Recent packages 7 | 8 |
9 | 10 | [% PROCESS "package_list.tt2" %] 11 |
12 | --------------------------------------------------------------------------------