├── app ├── .htaccess ├── Request.php ├── service.php ├── provider.php ├── model │ ├── VisitModel.php │ ├── ApiKeyModel.php │ ├── NoticeModel.php │ ├── StatsModel.php │ ├── SvipModel.php │ ├── SystemModel.php │ └── StatsDailyModel.php ├── event.php ├── middleware.php ├── AppService.php ├── middleware │ ├── VisitRecorder.php │ ├── AuthMiddleware.php │ └── ParseMiddleware.php ├── controller │ ├── Error.php │ ├── Statistics.php │ ├── WebApi.php │ ├── Common.php │ ├── Install.php │ ├── Admin.php │ └── Parse.php ├── ExceptionHandle.php ├── BaseController.php ├── database │ └── db.sql ├── utils │ ├── JWTUtils.php │ └── CurlUtils.php └── common.php ├── extend └── .gitignore ├── runtime └── .gitignore ├── public ├── static │ └── .gitignore ├── robots.txt ├── favicon.ico ├── .htaccess ├── router.php └── index.php ├── .gitignore ├── think ├── config ├── console.php ├── trace.php ├── middleware.php ├── session.php ├── cookie.php ├── filesystem.php ├── view.php ├── lang.php ├── app.php ├── cache.php ├── redis.php ├── log.php ├── route.php └── database.php ├── route ├── install.php └── api.php ├── composer.json ├── .travis.yml ├── README.md └── LICENSE.txt /app/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /extend/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /public/static/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f4team-cn/f4pan/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/Request.php: -------------------------------------------------------------------------------- 1 | console->run(); -------------------------------------------------------------------------------- /app/provider.php: -------------------------------------------------------------------------------- 1 | Request::class, 8 | 'think\exception\Handle' => ExceptionHandle::class, 9 | ]; 10 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Options +FollowSymlinks -Multiviews 3 | RewriteEngine On 4 | 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] 8 | 9 | -------------------------------------------------------------------------------- /app/model/VisitModel.php: -------------------------------------------------------------------------------- 1 | save($data); 12 | } 13 | } -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | [ 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /app/event.php: -------------------------------------------------------------------------------- 1 | [ 5 | ], 6 | 7 | 'listen' => [ 8 | 'AppInit' => [], 9 | 'HttpRun' => [], 10 | 'HttpEnd' => [], 11 | 'LogLevel' => [], 12 | 'LogWrite' => [], 13 | ], 14 | 15 | 'subscribe' => [ 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /app/middleware.php: -------------------------------------------------------------------------------- 1 | 'Html', 8 | // 读取的日志通道名 9 | 'channel' => '', 10 | ]; 11 | -------------------------------------------------------------------------------- /app/AppService.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'auth' => app\middleware\AuthMiddleware::class, 7 | 'parse' => app\middleware\ParseMiddleware::class, 8 | 'visit' => app\middleware\VisitRecorder::class 9 | ], 10 | // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 11 | 'priority' => [], 12 | ]; 13 | -------------------------------------------------------------------------------- /route/install.php: -------------------------------------------------------------------------------- 1 | ip(); 12 | $route = $request->pathinfo(); 13 | $model = new VisitModel(); 14 | $model->addVisit([ 15 | 'ip_address'=>$ip, 16 | 'route'=>$route 17 | ]); 18 | return $next($request); 19 | } 20 | } -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | 'PHPSESSID', 9 | // SESSION_ID的提交变量,解决flash上传跨域 10 | 'var_session_id' => '', 11 | // 驱动方式 支持file cache 12 | 'type' => 'file', 13 | // 存储连接标识 当type使用cache的时候有效 14 | 'store' => null, 15 | // 过期时间 16 | 'expire' => 1440, 17 | // 前缀 18 | 'prefix' => '', 19 | ]; 20 | -------------------------------------------------------------------------------- /app/controller/Error.php: -------------------------------------------------------------------------------- 1 | getRootPath() . 'install.lock')){ 12 | $install = true; 13 | }else{ 14 | $install = false; 15 | } 16 | return responseJson(-1, '接口不存在', ['author'=>'F4Team', 'version'=>env('app.version'), 'installed'=> $install, 'github'=> 'https://github.com/f4team-cn/f4pan', 'website'=>'https://www.f4team.cn/']); 17 | } 18 | } -------------------------------------------------------------------------------- /config/cookie.php: -------------------------------------------------------------------------------- 1 | 0, 8 | // cookie 保存路径 9 | 'path' => '/', 10 | // cookie 有效域名 11 | 'domain' => '', 12 | // cookie 启用安全传输 13 | 'secure' => false, 14 | // httponly设置 15 | 'httponly' => false, 16 | // 是否使用 setcookie 17 | 'setcookie' => true, 18 | // samesite 设置,支持 'strict' 'lax' 19 | 'samesite' => '', 20 | ]; 21 | -------------------------------------------------------------------------------- /config/filesystem.php: -------------------------------------------------------------------------------- 1 | 'local', 6 | // 磁盘列表 7 | 'disks' => [ 8 | 'local' => [ 9 | 'type' => 'local', 10 | 'root' => app()->getRuntimePath() . 'storage', 11 | ], 12 | 'public' => [ 13 | // 磁盘类型 14 | 'type' => 'local', 15 | // 磁盘路径 16 | 'root' => app()->getRootPath() . 'public/storage', 17 | // 磁盘路径对应的外部URL路径 18 | 'url' => '/storage', 19 | // 可见性 20 | 'visibility' => 'public', 21 | ], 22 | // 更多的磁盘配置信息 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /app/model/ApiKeyModel.php: -------------------------------------------------------------------------------- 1 | save($data); 14 | } 15 | 16 | public function getAllApiKeys(){ 17 | return $this->select(); 18 | } 19 | 20 | public function deleteApiKey(int $id){ 21 | return $this->where('id', $id)->delete(); 22 | } 23 | 24 | public function existApikey(string $key){ 25 | $apikey = $this->where('key', $key)->find(); 26 | return !is_null($apikey); 27 | } 28 | } -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | 'Think', 9 | // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 10 | 'auto_rule' => 1, 11 | // 模板目录名 12 | 'view_dir_name' => 'view', 13 | // 模板后缀 14 | 'view_suffix' => 'html', 15 | // 模板文件名分隔符 16 | 'view_depr' => DIRECTORY_SEPARATOR, 17 | // 模板引擎普通标签开始标记 18 | 'tpl_begin' => '{', 19 | // 模板引擎普通标签结束标记 20 | 'tpl_end' => '}', 21 | // 标签库标签开始标记 22 | 'taglib_begin' => '{', 23 | // 标签库标签结束标记 24 | 'taglib_end' => '}', 25 | ]; 26 | -------------------------------------------------------------------------------- /config/lang.php: -------------------------------------------------------------------------------- 1 | env('DEFAULT_LANG', 'zh-cn'), 9 | // 允许的语言列表 10 | 'allow_lang_list' => [], 11 | // 多语言自动侦测变量名 12 | 'detect_var' => 'lang', 13 | // 是否使用Cookie记录 14 | 'use_cookie' => true, 15 | // 多语言cookie变量 16 | 'cookie_var' => 'think_lang', 17 | // 多语言header变量 18 | 'header_var' => 'think-lang', 19 | // 扩展语言包 20 | 'extend_list' => [], 21 | // Accept-Language转义为对应语言包名称 22 | 'accept_language' => [ 23 | 'zh-hans-cn' => 'zh-cn', 24 | ], 25 | // 是否支持语言分组 26 | 'allow_group' => false, 27 | ]; 28 | -------------------------------------------------------------------------------- /public/router.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | // $Id$ 12 | 13 | if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) { 14 | return false; 15 | } else { 16 | $_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php'; 17 | 18 | require __DIR__ . "/index.php"; 19 | } 20 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | // [ 应用入口文件 ] 13 | namespace think; 14 | 15 | require __DIR__ . '/../vendor/autoload.php'; 16 | 17 | // 执行HTTP应用并响应 18 | $http = (new App())->http; 19 | 20 | $response = $http->run(); 21 | 22 | $response->send(); 23 | 24 | $http->end($response); 25 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_HOST', ''), 9 | // 应用的命名空间 10 | 'app_namespace' => '', 11 | // 是否启用路由 12 | 'with_route' => true, 13 | // 默认应用 14 | 'default_app' => 'index', 15 | // 默认时区 16 | 'default_timezone' => 'Asia/Shanghai', 17 | 18 | // 应用映射(自动多应用模式有效) 19 | 'app_map' => [], 20 | // 域名绑定(自动多应用模式有效) 21 | 'domain_bind' => [], 22 | // 禁止URL访问的应用列表(自动多应用模式有效) 23 | 'deny_app_list' => [], 24 | 25 | // 异常页面的模板文件 26 | 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', 27 | 28 | // 错误显示信息,非调试模式有效 29 | 'error_message' => '页面错误!请稍后再试~', 30 | // 显示错误信息 31 | 'show_error_msg' => true, 32 | ]; 33 | -------------------------------------------------------------------------------- /app/model/NoticeModel.php: -------------------------------------------------------------------------------- 1 | find($id); 13 | } 14 | 15 | public function getAllNotice() 16 | { 17 | return $this->select(); 18 | } 19 | 20 | public function addNotice(array $data) 21 | { 22 | return $this->save($data); 23 | } 24 | 25 | public function updateNotice(int $id, array $data) 26 | { 27 | return $this->where('id', $id)->update($data); 28 | } 29 | 30 | public function deleteNotice(int $id) 31 | { 32 | return $this->where('id', $id)->delete(); 33 | } 34 | 35 | public function existNotice(int $id): bool 36 | { 37 | $notice = $this->where('id', $id)->find(); 38 | return !is_null($notice); 39 | } 40 | } -------------------------------------------------------------------------------- /app/model/StatsModel.php: -------------------------------------------------------------------------------- 1 | value('total_parsing_traffic'); 13 | $total_parsing_traffic += $traffic; 14 | $this->where(1)->update(['total_parsing_traffic' => $total_parsing_traffic]); 15 | } 16 | 17 | function addParsingCount(){ 18 | //递增计数 19 | $total_parsing_count = $this->value('total_parsing_count'); 20 | $total_parsing_count += 1; 21 | $this->where(1)->update(['total_parsing_count' => $total_parsing_count]); 22 | } 23 | 24 | function addSpentSvipCount(){ 25 | $spent_svip_count = $this->value('spent_svip_count'); 26 | $spent_svip_count += 1; 27 | $this->where(1)->update(['spent_svip_count' => $spent_svip_count]); 28 | } 29 | } -------------------------------------------------------------------------------- /app/controller/Statistics.php: -------------------------------------------------------------------------------- 1 | count('DISTINCT ip_address'); 14 | $count = $model->count('ip_address'); 15 | return responseJson(200, 'success', ['ip_type' => $type, 'all_count' => $count]); 16 | } 17 | 18 | public function getIp(){ 19 | $ip = $this->request->request("ip"); 20 | //分页查询 21 | $size = $this->request->request("size")??10; 22 | $page = $this->request->request("page")??1; 23 | $model = new VisitModel(); 24 | if (empty($ip)) { 25 | return responseJson(-1, '未传递参数'); 26 | } 27 | $query = $model->page($page, $size)->select(); 28 | return responseJson(200, 'success', $query); 29 | } 30 | } -------------------------------------------------------------------------------- /app/middleware/AuthMiddleware.php: -------------------------------------------------------------------------------- 1 | header('Authorization'); 11 | if(empty($token)){ 12 | return responseJson(401 , '请进行管理员登录'); 13 | }else{ 14 | $res = JWTUtils::getPayloadAndVerify($token); 15 | if($res === 1){ 16 | return responseJson(401 , '请进行管理员登录'); 17 | }elseif($res === 2){ 18 | return responseJson(401 , '请进行管理员登录'); 19 | }elseif($res === 3){ 20 | return responseJson(401 , '您的登录过期'); 21 | }elseif($res === 4){ 22 | return responseJson(401 , '您的登录过期'); 23 | }elseif($res === 5){ 24 | return responseJson(401 , '您的登录过期'); 25 | } 26 | $request->login = $res; 27 | return $next($request); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | 'file', 10 | 11 | // 缓存连接方式配置 12 | 'stores' => [ 13 | 'file' => [ 14 | // 驱动方式 15 | 'type' => 'File', 16 | // 缓存保存目录 17 | 'path' => '', 18 | // 缓存前缀 19 | 'prefix' => '', 20 | // 缓存有效期 0表示永久缓存 21 | 'expire' => 0, 22 | // 缓存标签前缀 23 | 'tag_prefix' => 'tag:', 24 | // 序列化机制 例如 ['serialize', 'unserialize'] 25 | 'serialize' => [], 26 | ], 27 | 'redis' => [ 28 | 'type' => 'redis', 29 | 'port' => env('redis.port', 6379), 30 | 'host' => env('redis.host', '127.0.0.1'), 31 | 'password' => env('redis.pass', '') 32 | ] 33 | ] 34 | ]; 35 | -------------------------------------------------------------------------------- /config/redis.php: -------------------------------------------------------------------------------- 1 | 'data', 6 | // 数据库连接配置信息 7 | 'connections' => [ 8 | 'data' => [ 9 | 'host' => '127.0.0.1', 10 | 'port' => 6379, 11 | 'password' => '', 12 | 'select' => 0, 13 | 'timeout' => 3, 14 | 'options' => [], 15 | 'persistent' => false, 16 | ], 17 | 'poolDemo' => [ 18 | 'host' => '127.0.0.1', 19 | 'port' => 6379, 20 | 'password' => '', 21 | 'select' => 0, 22 | 'timeout' => 3, 23 | 'prefix' => null, 24 | 'options' => [], 25 | 'persistent' => false, 26 | // 连接池设置 27 | 'pool' => [ 28 | 'min_active' => 0, 29 | 'max_active' => 10, 30 | 'max_wait_time' => 5, 31 | 'max_idle_time' => 30, 32 | 'idle_check_interval' => 60, 33 | ], 34 | // 快速释放 35 | 'fast_freed' => false, 36 | // 自动丢弃 37 | 'auto_discard' => false, 38 | ], 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /config/log.php: -------------------------------------------------------------------------------- 1 | 'file', 9 | // 日志记录级别 10 | 'level' => [], 11 | // 日志类型记录的通道 ['error'=>'email',...] 12 | 'type_channel' => [], 13 | // 关闭全局日志写入 14 | 'close' => false, 15 | // 全局日志处理 支持闭包 16 | 'processor' => null, 17 | 18 | // 日志通道列表 19 | 'channels' => [ 20 | 'file' => [ 21 | // 日志记录方式 22 | 'type' => 'File', 23 | // 日志保存目录 24 | 'path' => '', 25 | // 单文件日志写入 26 | 'single' => false, 27 | // 独立日志级别 28 | 'apart_level' => [], 29 | // 最大日志文件数量 30 | 'max_files' => 0, 31 | // 使用JSON格式记录 32 | 'json' => false, 33 | // 日志处理 34 | 'processor' => null, 35 | // 关闭通道日志写入 36 | 'close' => false, 37 | // 日志输出格式化 38 | 'format' => '[%s][%s] %s', 39 | // 是否实时写入 40 | 'realtime_write' => false, 41 | ], 42 | // 其它日志通道配置 43 | ], 44 | 45 | ]; 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "topthink/think", 3 | "description": "the new thinkphp framework", 4 | "type": "project", 5 | "keywords": [ 6 | "framework", 7 | "thinkphp", 8 | "ORM" 9 | ], 10 | "homepage": "https://www.thinkphp.cn/", 11 | "license": "Apache-2.0", 12 | "authors": [ 13 | { 14 | "name": "liu21st", 15 | "email": "liu21st@gmail.com" 16 | }, 17 | { 18 | "name": "yunwuxin", 19 | "email": "448901948@qq.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.0.0", 24 | "topthink/framework": "^8.0", 25 | "topthink/think-orm": "^3.0", 26 | "topthink/think-filesystem": "^2.0", 27 | "lcobucci/jwt": "^4.0", 28 | "zxin/think-redis": "^2.0" 29 | }, 30 | "require-dev": { 31 | "symfony/var-dumper": ">=4.2", 32 | "topthink/think-trace": "^1.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "app\\": "app" 37 | }, 38 | "psr-0": { 39 | "": "extend/" 40 | } 41 | }, 42 | "config": { 43 | "preferred-install": "dist" 44 | }, 45 | "scripts": { 46 | "post-autoload-dump": [ 47 | "@php think service:discover", 48 | "@php think vendor:publish" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/middleware/ParseMiddleware.php: -------------------------------------------------------------------------------- 1 | request('req_id'); 11 | if(!isset($req_id)){ 12 | return responseJson(-1, "error, 缺少必要参数 req_id"); 13 | } 14 | $redis = \think\facade\Cache::store('redis'); 15 | if(!$redis->has($req_id)){ 16 | return responseJson(-1, "error, 请先使用密码"); 17 | } 18 | [$surl, $password] = explode('|',$redis->get($req_id)); 19 | $dir = $request->request('dir')??''; 20 | $isRoot = $request->request('isroot')??true; 21 | $fs_id = $request->request('fs_id')??''; 22 | $randsk = $request->request('randsk')??''; 23 | $share_id = $request->request('share_id')??''; 24 | $uk = $request->request('uk')??''; 25 | $short = $request->request('short')??false; 26 | $request->surl = $surl; 27 | $request->password = $password; 28 | $request->isroot = $isRoot; 29 | $request->dir = $dir; 30 | $request->surl = $surl; 31 | $request->fs_id = $fs_id; 32 | $request->randsk = $randsk; 33 | $request->share_id = $share_id; 34 | $request->uk = $uk; 35 | $request->short = $short; 36 | return $next($request); 37 | } 38 | } -------------------------------------------------------------------------------- /config/route.php: -------------------------------------------------------------------------------- 1 | '/', 9 | // URL伪静态后缀 10 | 'url_html_suffix' => 'html', 11 | // URL普通方式参数 用于自动生成 12 | 'url_common_param' => true, 13 | // 是否开启路由延迟解析 14 | 'url_lazy_route' => false, 15 | // 是否强制使用路由 16 | 'url_route_must' => false, 17 | // 合并路由规则 18 | 'route_rule_merge' => false, 19 | // 路由是否完全匹配 20 | 'route_complete_match' => false, 21 | // 访问控制器层名称 22 | 'controller_layer' => 'controller', 23 | // 空控制器名 24 | 'empty_controller' => 'Error', 25 | // 是否使用控制器后缀 26 | 'controller_suffix' => false, 27 | // 默认的路由变量规则 28 | 'default_route_pattern' => '[\w\.]+', 29 | // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 30 | 'request_cache_key' => false, 31 | // 请求缓存有效期 32 | 'request_cache_expire' => null, 33 | // 全局请求缓存排除规则 34 | 'request_cache_except' => [], 35 | // 默认控制器名 36 | 'default_controller' => 'Install', 37 | // 默认操作名 38 | 'default_action' => 'index', 39 | // 操作方法后缀 40 | 'action_suffix' => '', 41 | // 默认JSONP格式返回的处理方法 42 | 'default_jsonp_handler' => 'jsonpReturn', 43 | // 默认JSONP处理方法 44 | 'var_jsonp_handler' => 'callback', 45 | ]; 46 | -------------------------------------------------------------------------------- /app/ExceptionHandle.php: -------------------------------------------------------------------------------- 1 | env('DB_DRIVER', 'mysql'), 6 | 7 | // 自定义时间查询规则 8 | 'time_query_rule' => [], 9 | 10 | // 自动写入时间戳字段 11 | // true为自动识别类型 false关闭 12 | // 字符串则明确指定时间字段类型 支持 int timestamp datetime date 13 | 'auto_timestamp' => true, 14 | 15 | // 时间字段取出后的默认时间格式 16 | 'datetime_format' => 'Y-m-d H:i:s', 17 | 18 | // 时间字段配置 配置格式:create_time,update_time 19 | 'datetime_field' => '', 20 | 21 | // 数据库连接配置信息 22 | 'connections' => [ 23 | 'mysql' => [ 24 | // 数据库类型 25 | 'type' => env('DB_TYPE', 'mysql'), 26 | // 服务器地址 27 | 'hostname' => env('DB_HOST', '127.0.0.1'), 28 | // 数据库名 29 | 'database' => env('DB_NAME', ''), 30 | // 用户名 31 | 'username' => env('DB_USER', 'root'), 32 | // 密码 33 | 'password' => env('DB_PASS', ''), 34 | // 端口 35 | 'hostport' => env('DB_PORT', '3306'), 36 | // 数据库连接参数 37 | 'params' => [], 38 | // 数据库编码默认采用utf8 39 | 'charset' => env('DB_CHARSET', 'utf8'), 40 | // 数据库表前缀 41 | 'prefix' => env('DB_PREFIX', ''), 42 | 43 | // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) 44 | 'deploy' => 0, 45 | // 数据库读写是否分离 主从式有效 46 | 'rw_separate' => false, 47 | // 读写分离后 主服务器数量 48 | 'master_num' => 1, 49 | // 指定从服务器序号 50 | 'slave_no' => '', 51 | // 是否严格检查字段是否存在 52 | 'fields_strict' => true, 53 | // 是否需要断线重连 54 | 'break_reconnect' => false, 55 | // 监听SQL 56 | 'trigger_sql' => env('APP_DEBUG', true), 57 | // 开启字段缓存 58 | 'fields_cache' => false, 59 | ], 60 | ], 61 | ]; 62 | -------------------------------------------------------------------------------- /app/model/SvipModel.php: -------------------------------------------------------------------------------- 1 | where('state', '0')->where('svip_end_time', '>', time())->select(); 21 | } 22 | 23 | /** 获取所有账号 24 | * @return SvipModel[]|array|\think\Collection 25 | * @throws \think\db\exception\DataNotFoundException 26 | * @throws \think\db\exception\DbException 27 | * @throws \think\db\exception\ModelNotFoundException 28 | */ 29 | public function getAllList(){ 30 | return $this->select(); 31 | } 32 | 33 | /** 根据id获取SVIP信息 34 | * @param int $id 35 | * @return array|mixed 36 | * @throws \think\db\exception\DataNotFoundException 37 | * @throws \think\db\exception\DbException 38 | * @throws \think\db\exception\ModelNotFoundException 39 | */ 40 | public function getSvipById(int $id) 41 | { 42 | return $this->find($id); 43 | } 44 | 45 | /** 增加SVIP 46 | * @param array $data 47 | * @return bool 48 | */ 49 | public function addSvip(array $data) 50 | { 51 | return $this->save($data); 52 | } 53 | 54 | /** 更新SVIP信息 55 | * @param int $id 56 | * @param array $data 57 | * @return SvipModel 58 | */ 59 | public function updateSvip(int $id, array $data) 60 | { 61 | return $this->where('id', $id)->update($data); 62 | } 63 | 64 | /** 删除SVIP 65 | * @param int $id 66 | * @return bool 67 | */ 68 | public function deleteSvip(int $id) 69 | { 70 | return $this->where('id', $id)->delete(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | branches: 6 | only: 7 | - stable 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache 12 | 13 | before_install: 14 | - composer self-update 15 | 16 | install: 17 | - composer install --no-dev --no-interaction --ignore-platform-reqs 18 | - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip . 19 | - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0" 20 | - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0" 21 | - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0" 22 | - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0" 23 | - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0" 24 | - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0" 25 | - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0" 26 | - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0" 27 | - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0" 28 | - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip . 29 | 30 | script: 31 | - php think unit 32 | 33 | deploy: 34 | provider: releases 35 | api_key: 36 | secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw= 37 | file: 38 | - ThinkPHP_Core.zip 39 | - ThinkPHP_Full.zip 40 | skip_cleanup: true 41 | on: 42 | tags: true 43 | -------------------------------------------------------------------------------- /app/BaseController.php: -------------------------------------------------------------------------------- 1 | app = $app; 47 | $this->request = $this->app->request; 48 | 49 | // 控制器初始化 50 | $this->initialize(); 51 | } 52 | 53 | // 初始化 54 | protected function initialize() 55 | {} 56 | 57 | /** 58 | * 验证数据 59 | * @access protected 60 | * @param array $data 数据 61 | * @param string|array $validate 验证器名或者验证规则数组 62 | * @param array $message 提示信息 63 | * @param bool $batch 是否批量验证 64 | * @return array|string|true 65 | * @throws ValidateException 66 | */ 67 | protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false) 68 | { 69 | if (is_array($validate)) { 70 | $v = new Validate(); 71 | $v->rule($validate); 72 | } else { 73 | if (strpos($validate, '.')) { 74 | // 支持场景 75 | [$validate, $scene] = explode('.', $validate); 76 | } 77 | $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); 78 | $v = new $class(); 79 | if (!empty($scene)) { 80 | $v->scene($scene); 81 | } 82 | } 83 | 84 | $v->message($message); 85 | 86 | // 是否批量验证 87 | if ($batch || $this->batchValidate) { 88 | $v->batch(true); 89 | } 90 | 91 | return $v->failException(true)->check($data); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | F4Team 3 |

4 | 5 | 6 | 7 | *************************************** 8 | 9 |

10 | 11 | `F4Pan`,是一个获取下载链接的工具 12 | 13 |

14 | 15 | 16 | ## ⚠ 免责声明 17 | * `F4Pan`(下称本项目)使用的接口全部来自于官方,无任何破坏接口的行为
18 | * 本项目所有代码全部开源,仅供学习参考使用,请遵守相关的法律法规,禁止商用,若无视声明使用此项目所造成的一切后果均与作者无关
19 | * 本项目需要登录账号,具有一定风险,包括但不限于限速,封号,限制相关功能等
20 | * 本项目,包括其开发者、贡献者和附属个人或实体,特此明确否认与任何形式的非法行为有任何关联、支持或认可。本免责声明适用于可能违反地方、国家或国际法律、法规或道德准则的F4Pan项目的任何使用或应用。
21 | * 本项目是一个开源软件项目,旨在促进其预期用例中的合法和道德应用程序。每个用户都有责任确保其使用F4Pan符合其管辖范围内的所有适用法律和法规。
22 | * 对于用户违反法律或从事任何形式的非法活动的任何行为,本项目的开发者和贡献者不承担任何责任。用户对自己的行为和使用F4Pan可能产生的任何后果负全部责任。
23 | * 此外,本项目(包括其开发人员、贡献者和用户)提供的任何讨论、建议或指导都不应被解释为法律建议。强烈建议用户寻求独立的法律顾问,以了解其行为的法律影响,并确保遵守有关的法律和条例。
24 | * 通过使用或访问本项目,用户承认并同意免除开发人员、贡献者和附属个人或实体因使用或滥用该项目而产生的任何和所有责任,包括因其行为而产生的任何法律后果。
25 | * 请负责任地、依法使用本项目。 26 | 27 | 28 | ## 🚧 所需环境 29 | * PHP >= 8.0 30 | * Mysql 31 | * Redis 32 | * Curl 33 |
⚠ 安装Mysql与Redis后若还未通过环境检查请在对应版本的`php.ini`中启用对应的拓展,需要的PHP拓展有`fileinfo`和`redis` 34 | 35 | 36 | ## 🔧 安装 37 | 38 | 本项目使用了`thinkphp8.0`框架
39 | Nginx伪静态(单独部署后端): 40 | ``` 41 | location ~* (runtime|application)/{ 42 | return 403; 43 | } 44 | location / { 45 | if (!-e $request_filename){ 46 | rewrite ^(.*)$ /index.php?s=$1 last; break; 47 | } 48 | } 49 | ``` 50 | Nginx伪静态(前端+后端): 51 | ``` 52 | location ~* (runtime|application)/{ 53 | return 403; 54 | } 55 | location /api { 56 | rewrite ^(.*)$ /index.php?s=$1 last; break; 57 | } 58 | location / { 59 | index index.html; 60 | try_files $uri $uri/ /index.html; 61 | } 62 | ``` 63 | ### 🔧 手动构建 64 | 本项目`前后端分离`的架构
65 | 可从`Releases`页面下载完整包
66 | 67 | 1. 解压到网站目录下 68 | 2. 设置运行目录为`/public` 69 | 3. 连接服务器ssh,cd到网站目录,执行`composer install`命令,等待依赖安装完成 70 | 4. 设置伪静态 71 | 5. 访问`http(s)://你的域名/#/install`跟随引导进行安装 72 | 73 | 如果使用`宝塔面板`进行安装,在执行`composer install`前应去`禁用函数`页面删除`putenv`和`proc_open`函数 74 | 75 | ## ⚠️ Tips 76 | 动态密钥获取方法: 77 | 1. 登录后台,进入apikey管理页面,新增一个apikey 78 | 2. GET访问`/api/public/get_parse_key?apikey={apikey}`获取动态解析密钥 79 | 80 | ## 📦 前端更新方法 81 | 前往[f4pan-web](https://github.com/f4team-cn/f4pan-web)仓库的`actions`页面下载最新的构建版本 82 | 解压到`public`文件夹下更新 83 | 84 | ## ✔️ 反馈 85 | ### 欢迎提交BUG 86 | 可通过`Issues`或 [Telegram](https://t.me/f4pan_project) 与我们取得联系 87 | 88 | ## 🔗 相关仓库 89 | 前端 [f4pan-web](https://github.com/f4team-cn/f4pan-web) 90 | 91 | 后端 [f4pan](￶https://github.com/f4team-cn/f4pan) 92 | 93 | # ©️ 最终解释权归F4Team所有 94 | 进入我们的[官网](https://www.f4team.cn/) 95 | -------------------------------------------------------------------------------- /app/database/db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `svip` ( 2 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '账号ID', 3 | `name` varchar(255) NOT NULL COMMENT '用户名', 4 | `state` enum('0','-1') NOT NULL COMMENT '状态 0 正常 -1 不可使用', 5 | `cookie` varchar(2048) NOT NULL COMMENT '身份信息', 6 | -- `local_state` TEXT NOT NULL COMMENT 'local_state用户信息', 7 | -- `access_token` varchar(2048) NOT NULL COMMENT 'access_token', 8 | `add_time` int(11) NOT NULL COMMENT '添加时间', 9 | `svip_end_time` int(11) NOT NULL COMMENT 'SVIP过期时间', 10 | `vip_type` enum('普通用户','普通会员','超级会员') NOT NULL COMMENT 'VIP类型', 11 | PRIMARY KEY (`id`), 12 | INDEX `idx_state` (`state`) 13 | ) ENGINE=InnoDB AUTO_INCREMENT=172 DEFAULT CHARSET=utf8mb4; 14 | 15 | CREATE TABLE `system` ( 16 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '系统ID', 17 | `admin_password` VARCHAR(255) NOT NULL COMMENT '管理员密码', 18 | `requires_key` enum('fixed','dynamic','none') NOT NULL DEFAULT 'dynamic' COMMENT '是否需要密钥(动态或固定)', 19 | `notice_id` INT DEFAULT 0 COMMENT '使用的公告ID', 20 | `key_last_time` INT DEFAULT 300 COMMENT '动态密钥有效时长(秒)', 21 | `fixed_key` VARCHAR(255) NULL COMMENT '固定的密钥值(如果动态密钥禁用)', 22 | `real_url_last_time` INT DEFAULT 1800 COMMENT '真实链接存储时间(秒)', 23 | `parse_ua` VARCHAR(255) NULL COMMENT '解析时使用的UA', 24 | `normal_cookie` TEXT NULL COMMENT '普通Cookie', 25 | `is_active` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为当前活动配置', 26 | PRIMARY KEY (`id`) 27 | ); 28 | 29 | CREATE TABLE `notice` ( 30 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '公告ID', 31 | `title` varchar(255) NOT NULL COMMENT '公告标题', 32 | `content` varchar(2048) NOT NULL COMMENT '公告内容', 33 | `add_time` int(11) NOT NULL COMMENT '添加时间', 34 | PRIMARY KEY (`id`) 35 | ); 36 | 37 | CREATE TABLE `stats` ( 38 | `total_parsing_traffic` BIGINT NOT NULL DEFAULT 0 COMMENT '总解析流量', 39 | `total_parsing_count` INT NOT NULL DEFAULT 0 COMMENT '总解析文件数', 40 | `spent_svip_count` INT NOT NULL DEFAULT 0 COMMENT '已消耗SVIP数量', 41 | `last_updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', 42 | UNIQUE INDEX(`total_parsing_traffic`, `total_parsing_count`, `spent_svip_count`) 43 | ) ENGINE=InnoDB; 44 | -- 初始化统计数据 45 | INSERT INTO `stats` (`total_parsing_traffic`, `total_parsing_count`, `spent_svip_count`) VALUES (0, 0, 0); 46 | 47 | CREATE TABLE `visits` ( 48 | id INT AUTO_INCREMENT PRIMARY KEY, 49 | ip_address VARCHAR(45) NOT NULL, 50 | route VARCHAR(255) NOT NULL, 51 | visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP 52 | ); 53 | 54 | CREATE TABLE `api_keys`( 55 | `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', 56 | `key` VARCHAR(32) NOT NULL COMMENT 'API Key', 57 | `use_count` INT NOT NULL DEFAULT 0 COMMENT '使用次数', 58 | UNIQUE (`key`) 59 | ); 60 | 61 | CREATE TABLE `stats_daily` ( 62 | `stat_date` DATE NOT NULL COMMENT '统计日期', 63 | `parsing_traffic` BIGINT NOT NULL DEFAULT 0 COMMENT '当日解析流量', 64 | `parsing_count` INT NOT NULL DEFAULT 0 COMMENT '当日解析文件数', 65 | `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 66 | PRIMARY KEY (`stat_date`) 67 | ) ENGINE=InnoDB COMMENT='每日统计表'; -------------------------------------------------------------------------------- /app/model/SystemModel.php: -------------------------------------------------------------------------------- 1 | select(); 20 | } 21 | 22 | /** 获取活动表 23 | * @return SystemModel[]|array|\think\Collection 24 | * @throws \think\db\exception\DataNotFoundException 25 | * @throws \think\db\exception\DbException 26 | * @throws \think\db\exception\ModelNotFoundException 27 | */ 28 | public function getAchieve(){ 29 | return $this->where('is_active', 1)->select(); 30 | } 31 | 32 | /** 获取ua 33 | * @return mixed 34 | * @throws \think\db\exception\DataNotFoundException 35 | * @throws \think\db\exception\DbException 36 | * @throws \think\db\exception\ModelNotFoundException 37 | */ 38 | public static function getUa(){ 39 | return self::where('is_active', 1)->select()[0]['parse_ua']; 40 | } 41 | 42 | /** 获取普通cookie 43 | * @return mixed 44 | * @throws \think\db\exception\DataNotFoundException 45 | * @throws \think\db\exception\DbException 46 | * @throws \think\db\exception\ModelNotFoundException 47 | */ 48 | public static function getNormalCookie(){ 49 | return self::where('is_active', 1)->select()[0]['normal_cookie']; 50 | } 51 | /** 通过id获取表内容 52 | * @param int $id 53 | * @return array|mixed 54 | * @throws \think\db\exception\DataNotFoundException 55 | * @throws \think\db\exception\DbException 56 | * @throws \think\db\exception\ModelNotFoundException 57 | */ 58 | public function getSystemById(int $id){ 59 | return $this->find($id); 60 | } 61 | 62 | /** 保存系统表 63 | * @param array $data 64 | * @return bool 65 | */ 66 | public function addSystem(array $data){ 67 | return $this->save($data); 68 | } 69 | 70 | /** 启用系统表BY ID 71 | * @param int $id 72 | * @return bool 73 | */ 74 | public function useSystemById(int $id): bool 75 | { 76 | // 启用此表的同时把除该id外的所有表is_active置为0 77 | $this->where('id', '<>', $id)->update(['is_active' => 0]); 78 | $status = $this->where('id', $id)->update(['is_active' => 1]); 79 | if (!$status || $status == 0) { 80 | return false; 81 | } 82 | return true; 83 | } 84 | 85 | /** 修改系统表 86 | * @param int $id 87 | * @param array $data 88 | * @return SystemModel 89 | */ 90 | public function updateSystem(int $id, array $data){ 91 | return $this->where('id', $id)->update($data); 92 | } 93 | 94 | /** 删除系统表 95 | * @param int $id 96 | * @return bool 97 | */ 98 | public function deleteSystem(int $id){ 99 | return $this->where('id', $id)->delete(); 100 | } 101 | } -------------------------------------------------------------------------------- /app/controller/WebApi.php: -------------------------------------------------------------------------------- 1 | curl = new CurlUtils(); 17 | } 18 | 19 | public function getQrcode(){ 20 | $url = "https://passport.baidu.com/v2/api/getqrcode?lp=pc&qrloginfrom=pc&gid=197993A-8152-4BFA-A432-E2D84CF46E3C&oauthLog=&apiver=v3&tt=1675336415350&tpl=netdisk&logPage=traceId:pc_loginv4_1675336222,logPage:loginv4&_=1675336415355"; 21 | $data = CurlUtils::get($url)->obj(true); 22 | $imgurl = $data["imgurl"]; 23 | $imgurl = "http://".$imgurl; 24 | $sign = $data["sign"]; 25 | return responseJson(200,"获取成功",array("imgurl"=>$imgurl, "sign"=>$sign)); 26 | } 27 | public function unicast(){ 28 | $sign = $this->request->request('sign'); 29 | if(!$sign){ 30 | return responseJson(-1, '无sign传入'); 31 | } 32 | $url = "https://passport.baidu.com/channel/unicast?channel_id=".$sign."&gid=9B53B82-019B-47C2-AA3E-9D5D9D10F1E5&tpl=netdisk&_sdkFrom=1&apiver=v3&tt=1714656808250&_=1714656808250"; 33 | $data = CurlUtils::timeout(60)->get($url)->obj(true); 34 | // var_dump($data); 35 | if($data["errno"] == 0){ 36 | $channel_v = $data["channel_v"]; 37 | $channel_v = json_decode($channel_v,true); 38 | if($channel_v["status"] == 0){ 39 | $v = $channel_v["v"]; 40 | return responseJson(200,"获取成功",array("bduss"=>$v)); 41 | } 42 | return responseJson(-1,"请在百度网盘客户端确认登陆"); 43 | } 44 | return responseJson(-2,"请在百度网盘客户端扫码"); 45 | } 46 | public function qrcodeLogin(){ 47 | $bduss = $this->request->request('bduss'); 48 | $time = time(); 49 | $ttime = time().rand(111,999); 50 | $url = 'https://passport.baidu.com/v3/login/main/qrbdusslogin?v='.$ttime.'&bduss='.$bduss.'&u=https%253A%252F%252Fpan.baidu.com%252Fdisk%252Fmain%253Ffrom%253DhomeFlow%2523%252Findex&loginVersion=v4&qrcode=1&tpl=netdisk&apiver=v3&tt='.$ttime.'&traceid=&time='.$time.'&alg=v3&sig=dCtHYWdSUjNXL3pzTk1Cb0F2ZmxWdGJObUNTQTR4VlRITndidlhzMmI1L3Z0ZTR0NHpneXdlVDhXWE1jbWgwdg%3D%3D&elapsed=176&shaOne=006de51bd08c3ee46a42b84cf525dac1ddcfa6dc&rinfo=%7B%22fuid%22%3A%22afead616eeadbcebf9985529945cca45%22%7D'; 51 | $url_res = CurlUtils::get($url)->head()['Set-Cookie']; 52 | $cookie = ""; 53 | foreach($url_res as $k){ 54 | $cookie .= $k."; "; 55 | } 56 | $url = 'https://passport.baidu.com/v3/login/api/auth/?return_type=5&tpl=netdisk&u=https%3A%2F%2Fpan.baidu.com%2Fdisk%2Fmain'; 57 | $loadurl = CurlUtils::cookie($cookie)->get($url)->location(false)->all(); 58 | $stoken = CurlUtils::cookie($cookie)->location(false)->get($loadurl['headers']['Location'])->all(); 59 | preg_match('/BDUSS=(.*?);/i', $cookie, $matches); 60 | if(!@$matches[1]){ 61 | return responseJson(-1,"获取失败"); 62 | } 63 | $cookie = 'BDUSS='.$matches[1].';STOKEN='.$stoken['cookie']['STOKEN']; 64 | return responseJson(200,"获取成功",array("cookie"=>$cookie)); 65 | } 66 | } -------------------------------------------------------------------------------- /app/model/StatsDailyModel.php: -------------------------------------------------------------------------------- 1 | ensureRecordExists($date); 21 | 22 | $this->where('stat_date', $date) 23 | ->inc('parsing_traffic', $traffic) 24 | ->update(); 25 | } 26 | 27 | /** 28 | * 增加当日解析文件数 29 | * @param int $count 30 | */ 31 | public function addParsingCount(int $count = 1) 32 | { 33 | $date = date('Y-m-d'); 34 | $this->ensureRecordExists($date); 35 | 36 | $this->where('stat_date', $date) 37 | ->inc('parsing_count', $count) 38 | ->update(); 39 | } 40 | 41 | /** 42 | * 确保指定日期的记录存在 43 | * @param string $date 日期(格式:Y-m-d) 44 | */ 45 | private function ensureRecordExists(string $date) 46 | { 47 | $today = $this->find($date); 48 | if(!$today){ 49 | Db::name($this->name) 50 | ->insert([ 51 | 'stat_date' => $date, 52 | 'parsing_traffic' => 0, 53 | 'parsing_count' => 0, 54 | ]); 55 | } 56 | } 57 | 58 | /** 59 | * 获取某日统计数据 60 | * @param string $date 61 | */ 62 | public function getByDate(string $date) 63 | { 64 | $date = date('Y-m-d'); 65 | $this->ensureRecordExists($date); 66 | return $this->find($date); 67 | } 68 | 69 | 70 | public function getPastDaysData(int $days) 71 | { 72 | // 参数验证 73 | $days = max(1, $days); // 至少获取1天数据 74 | 75 | // 计算日期范围 76 | $endDate = date('Y-m-d'); 77 | $startDate = date('Y-m-d', strtotime("-" . ($days - 1) . " days")); 78 | 79 | // 执行查询 80 | $data = $this->whereBetween('stat_date', [$startDate, $endDate]) 81 | ->order('stat_date', 'DESC') 82 | ->select() 83 | ->toArray(); 84 | 85 | //添加format 86 | foreach ($data as $k=>$va){ 87 | $data[$k] += ['format_parsing_traffic' => formatSize($data[$k]["parsing_traffic"])]; 88 | } 89 | // 处理可能存在的日期间隙(补全缺失日期) 90 | return $this->fillDateGaps($data, $startDate, $endDate); 91 | } 92 | 93 | /** 94 | * 补全日期间隙数据 95 | */ 96 | private function fillDateGaps(array $data, string $startDate, string $endDate) 97 | { 98 | $filled = []; 99 | $current = $endDate; 100 | 101 | // 生成完整日期范围 102 | while ($current >= $startDate) { 103 | $found = array_filter($data, function($item) use ($current) { 104 | return $item['stat_date'] === $current; 105 | }); 106 | 107 | if (!empty($found)) { 108 | $filled[] = array_shift($found); 109 | } else { 110 | $filled[] = [ 111 | 'stat_date' => $current, 112 | 'parsing_traffic' => 0, 113 | 'parsing_count' => 0, 114 | 'format_parsing_traffic' => formatSize(0) 115 | ]; 116 | } 117 | 118 | $current = date('Y-m-d', strtotime($current . " -1 day")); 119 | } 120 | 121 | return $filled; 122 | } 123 | } -------------------------------------------------------------------------------- /app/utils/JWTUtils.php: -------------------------------------------------------------------------------- 1 | 17 | * @link https://www.yingyya.cn/ 18 | * @link https://www.f4team.cn/ 19 | */ 20 | class JWTUtils 21 | { 22 | private static string $KEY = 'ba06aefedf58341f2e193bf8ac2b3bd88c710d22acac'; 23 | private static string $ISSUED = 'f4team.cn'; 24 | private static string $PERMITTED = 'f4pan.f4team.cn'; 25 | 26 | private static function getJWTOb(): Configuration 27 | { 28 | return Configuration::forSymmetricSigner(new Sha256(),InMemory::base64Encoded(self::$KEY)); 29 | } 30 | 31 | /** 32 | * 获取 Token 33 | * @param array $bind token 负载 34 | * @param bool $refreshToken 是否是刷新token 35 | * @return string 36 | */ 37 | public static function getToken(array $bind = [], bool $refreshToken = false): string { 38 | $jwt = self::getJWTOb(); 39 | $builder = $jwt->builder(); 40 | $builder->withClaim('scope', $refreshToken ? 'refresh' : 'user-access'); 41 | foreach ($bind as $key => $value) { 42 | $builder->withClaim($key, $value); 43 | } 44 | $issuedAt = new \DateTimeImmutable(); 45 | return $builder 46 | ->issuedBy(self::$ISSUED) 47 | ->permittedFor(self::$PERMITTED) 48 | ->issuedAt($issuedAt) 49 | ->canOnlyBeUsedAfter($issuedAt->modify('-1 second')) // 提前一秒为了登录后token立即生效 50 | ->expiresAt($issuedAt->modify($refreshToken ? '+16 hour' : '+12 hour')) 51 | ->getToken($jwt->signer(), $jwt->signingKey())->toString(); 52 | } 53 | 54 | /** 55 | * 验证并获取 Token 负载 56 | * @param string $t 57 | * @return int|array int 验证失败 58 | */ 59 | public static function getPayloadAndVerify(string $t): int | array { 60 | $jwt = self::getJWTOb(); 61 | $token = $jwt->parser()->parse($t); 62 | if ($token instanceof \UnencryptedToken) { 63 | // 解析失败 64 | return 1; 65 | } 66 | $jwt->setValidationConstraints(new IssuedBy(self::$ISSUED)); 67 | try { 68 | $jwt->validator()->assert($token, ...$jwt->validationConstraints()); 69 | } catch (RequiredConstraintsViolated) { 70 | // 非本服务器签发的 token 71 | return 2; 72 | } 73 | $timezone = new \DateTimeZone('Asia/Shanghai'); 74 | $time = new SystemClock($timezone); 75 | $jwt->setValidationConstraints(new ValidAt($time)); 76 | try { 77 | $jwt->validator()->assert($token, ...$jwt->validationConstraints()); 78 | } catch (RequiredConstraintsViolated) { 79 | // token 过期 80 | return 3; 81 | } 82 | $validate_signed = new SignedWith(new Sha256(), InMemory::base64Encoded(self::$KEY)); 83 | $jwt->setValidationConstraints($validate_signed); 84 | try { 85 | $jwt->validator()->assert($token, ...$jwt->validationConstraints()); 86 | } catch (RequiredConstraintsViolated) { 87 | // 签名错误 88 | return 4; 89 | } 90 | // 验证成功 获取负载信息 91 | try { 92 | return json_decode(base64_decode($token->claims()->toString()), true); 93 | } catch (\Exception) { 94 | return 5; // 未知错误 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /route/api.php: -------------------------------------------------------------------------------- 1 | middleware('parse'); 10 | Route::get('s/', '\\app\\controller\\Parse@shortUrlRedirect')->pattern(['code'=>'.*']); 11 | }); 12 | //管理员系统 13 | //需要校验登录,中间件完成 14 | Route::group('admin', function () { 15 | // 认证与登录 16 | Route::group('auth', function () { 17 | Route::post('login', '\\app\\controller\\Admin@login'); 18 | }); 19 | // API密钥管理 20 | Route::group('api_keys', function () { 21 | Route::get('generate', '\\app\\controller\\Admin@generateApiKey'); 22 | Route::get('delete', '\\app\\controller\\Admin@deleteApiKey'); 23 | Route::get('', '\\app\\controller\\Admin@getApiKey'); 24 | })->middleware(['auth']); 25 | // SVIP管理 26 | Route::group('svips', function () { 27 | Route::post('add', '\\app\\controller\\Admin@addSvip'); 28 | Route::post('delete', '\\app\\controller\\Admin@deleteSvip'); 29 | Route::post('update', '\\app\\controller\\Admin@updateSvip'); 30 | Route::get('list', '\\app\\controller\\Admin@getSvipList'); 31 | Route::get('all', '\\app\\controller\\Admin@getAllList'); 32 | })->middleware(['auth']); 33 | // 公告管理 34 | Route::group('notices', function () { 35 | Route::get('all', '\\app\\controller\\Admin@getAllNotice'); 36 | Route::post('add', '\\app\\controller\\Admin@addNotice'); 37 | Route::post('update', '\\app\\controller\\Admin@updateNotice'); 38 | Route::post('delete', '\\app\\controller\\Admin@deleteNotice'); 39 | Route::post('use', '\\app\\controller\\Admin@useNotice'); 40 | })->middleware(['auth']); 41 | // 系统管理 42 | Route::group('systems', function () { 43 | Route::get('all', '\\app\\controller\\Admin@getAllSystem'); 44 | Route::post('add', '\\app\\controller\\Admin@addSystem'); 45 | Route::post('update', '\\app\\controller\\Admin@updateSystem'); 46 | Route::post('delete', '\\app\\controller\\Admin@deleteSystem'); 47 | Route::post('use', '\\app\\controller\\Admin@useSystem'); 48 | })->middleware(['auth']); 49 | // 统计管理 50 | Route::group('statistics', function () { 51 | Route::get('get_ip', '\\app\\controller\\Statistics@getIp'); 52 | Route::get('get_ip_count', '\\app\\controller\\Statistics@getIpCount'); 53 | })->middleware(['auth']); 54 | }); 55 | //公共接口 56 | Route::group('public', function () { 57 | Route::get('get_status', '\\app\\controller\\Common@getStatus'); 58 | Route::get('get_past_daily_data', '\\app\\controller\\Common@getPastDailyData'); 59 | Route::get('get_system', '\\app\\controller\\Common@getSystem'); 60 | Route::get('get_notice', '\\app\\controller\\Common@getNotice'); 61 | Route::get('get_parse_key', '\\app\\controller\\Common@getParseKey'); 62 | Route::get('use_parse_key', '\\app\\controller\\Common@useParseKey'); 63 | }); 64 | Route::group('web_api', function () { 65 | Route::get('get_qrcode', '\\app\\controller\\WebApi@getQrcode'); 66 | Route::get('unicast', '\\app\\controller\\WebApi@unicast'); 67 | Route::get('qrcode_login', '\\app\\controller\\WebApi@qrcodeLogin'); 68 | }); 69 | })->middleware('visit'); 70 | if (\think\facade\Env::get('OFFICIAL_DOCKER') == true) { 71 | Route::get('/', function () { 72 | return file_get_contents(app()->getRootPath() . 'public/index.html'); 73 | }); 74 | } 75 | Route::import(['route/install']); 76 | Route::miss('\\app\\controller\\Error@index'); 77 | -------------------------------------------------------------------------------- /app/controller/Common.php: -------------------------------------------------------------------------------- 1 | where(1)->select()->toArray()[0]; 18 | $format_size = formatSize($info['total_parsing_traffic']); 19 | $stats = new StatsDailyModel(); 20 | $info_daily = $stats->getByDate(date('Y-m-d'))->toArray(); 21 | if (!$info_daily) { 22 | $todayFlow = 0; 23 | }else{ 24 | $todayFlow = $info_daily["parsing_traffic"]; 25 | } 26 | $info['total_parsing_traffic_format'] = $format_size; 27 | $info['today_parsing_traffic_format'] = formatSize($todayFlow); 28 | $info["today"] = $info_daily; 29 | return responseJson(200, "success", $info); 30 | } 31 | 32 | public function getPastDailyData(){ 33 | $past_days = $this->request->param('past_days'); 34 | if (empty($past_days)) { 35 | return responseJson(-1, '未传递数据'); 36 | } 37 | 38 | $stats = new StatsDailyModel(); 39 | $info_daily = $stats->getPastDaysData($past_days); 40 | return responseJson(200, "success", $info_daily); 41 | } 42 | 43 | public function getSystem() 44 | { 45 | if (!file_exists(app()->getRootPath() . 'install.lock')){ 46 | return responseJson(-1 , '系统还未安装'); 47 | } 48 | $model = new SystemModel(); 49 | $active = $model->getAchieve(); 50 | $system = $active->toArray(); 51 | unset($system[0]["normal_cookie"]); 52 | unset($system[0]["admin_password"]); 53 | unset($system[0]["fixed_key"]); 54 | return responseJson(200, '获.取.了.', $system[0]); 55 | } 56 | 57 | public function getNotice() 58 | { 59 | $model = new NoticeModel(); 60 | $sysmodel = new SystemModel(); 61 | //获取active系统表内notice_id对应的公告 62 | $active = $sysmodel->getAchieve()->toArray()[0]; 63 | if ($active['notice_id'] == 0) { 64 | return responseJson(-1, '未.设.置.'); 65 | } 66 | $notice = $model->getNoticeById($active['notice_id']); 67 | return responseJson(200, '获.取.了.', $notice); 68 | } 69 | 70 | public function getParseKey(){ 71 | $apikey_model = new ApiKeyModel(); 72 | $apikey = $this->request->param('apikey'); 73 | if (empty($apikey)) { 74 | return responseJson(-1, '未.传.递.K.E.Y.'); 75 | } 76 | if ($apikey_model->existApikey($apikey)) { 77 | $system_model = new SystemModel(); 78 | $system = $system_model->getAchieve()->toArray()[0]; 79 | $key = randomNumKey(); 80 | $redis = \think\facade\Cache::store('redis'); 81 | $redis->set($key, $apikey, $system['key_last_time']); 82 | $apikey_model->where('key', $apikey)->update(['use_count' => $apikey_model->where('key', $apikey)->value('use_count') + 1]); 83 | return responseJson(200, '获.取.了.', substr($key, -6)); 84 | } 85 | return responseJson(-1, '未.查.到.K.E.Y.'); 86 | } 87 | 88 | public function useParseKey(){ 89 | $parse_key = $this->request->param('parse_key'); 90 | $redis = \think\facade\Cache::store('redis'); 91 | $model = new SystemModel(); 92 | $system = $model->getAchieve()->toArray()[0]; 93 | $surl = $this->request->param('surl'); 94 | $pwd = $this->request->param('pwd'); 95 | if (empty($surl)) { 96 | return responseJson(-1, '未传递参数'); 97 | } 98 | if ($system['requires_key'] == 'dynamic') { 99 | if ($redis->has('f4pan_parse_key_' . $parse_key)) { 100 | $redis->delete($parse_key); 101 | $req_id = randomKey("f4pan_req_id_"); 102 | $redis->set($req_id, $surl . '|' . $pwd, \think\facade\App::isDebug() ? 60 * 60 * 12 : 300); 103 | $redis->delete('f4pan_parse_key_' . $parse_key); 104 | return responseJson(200, '已使用KEY', $req_id); 105 | } 106 | return responseJson(-1, '未查到KEY'); 107 | }elseif ($system['requires_key'] == 'fixed'){ 108 | $key = $system['fixed_key']; 109 | if($key == $parse_key){ 110 | $req_id = randomKey("f4pan_req_id_"); 111 | $redis->set($req_id, $surl . '|' . $pwd, \think\facade\App::isDebug() ? 60 * 60 * 12 : 300); 112 | $redis->delete('f4pan_parse_key_' . $parse_key); 113 | return responseJson(200, '已使用KEY', $req_id); 114 | }else{ 115 | return responseJson(-1, 'KEY错误'); 116 | } 117 | }elseif ($system['requires_key'] == 'none'){ 118 | $req_id = randomKey("f4pan_req_id_"); 119 | $redis->set($req_id, $surl . '|' . $pwd, \think\facade\App::isDebug() ? 60 * 60 * 12 : 300); 120 | $redis->delete('f4pan_parse_key_' . $parse_key); 121 | return responseJson(200, '已生成req_id', $req_id); 122 | } 123 | return responseJson(-1, '未知错误'); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/controller/Install.php: -------------------------------------------------------------------------------- 1 | ='); 14 | $ext_check['php_version'] = phpversion(); 15 | 16 | //检查所需环境 17 | $ext = [ 18 | 'pdo_mysql', 19 | 'redis', 20 | 'curl' 21 | ]; 22 | foreach ($ext as $v){ 23 | $ext_check['ext'][$v] = extension_loaded($v); 24 | } 25 | //如果环境检查有一项不通过则返回错误 26 | foreach ($ext_check['ext'] as $k=>$v){ 27 | if (!$v || !$php_version){ 28 | return responseJson(-1 , '环境检查未通过', $ext_check); 29 | } 30 | } 31 | return responseJson(1 , '环境检查通过', $ext_check); 32 | } 33 | 34 | public function testDb(){ 35 | if(!extension_loaded('pdo_mysql')){ 36 | return responseJson(-1 , '请安装mysql扩展'); 37 | } 38 | $db_config = [ 39 | 'hostname' => $this->request->param('hostname')?? '127.0.0.1', 40 | 'database' => $this->request->param('database')?? 'f4pan', 41 | 'username' => $this->request->param('username'), 42 | 'password' => $this->request->param('password'), 43 | 'hostport' => $this->request->param('port')?? 3306, 44 | ]; 45 | //测试链接 pdo 46 | try{ 47 | $pdo = new \PDO("mysql:host={$db_config['hostname']};port={$db_config['hostport']};}", $db_config['username'], $db_config['password']); 48 | $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$db_config['database']}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"); 49 | }catch (\PDOException $e){ 50 | return responseJson(-1 , '数据库链接失败', $e->getMessage()); 51 | } 52 | return responseJson(1 , '数据库链接成功'); 53 | } 54 | 55 | public function testRedis(){ 56 | if(!extension_loaded('redis')){ 57 | return responseJson(-1 , '请安装redis扩展'); 58 | } 59 | $redis_config = [ 60 | 'host' => $this->request->param('hostname')?? '127.0.0.1', 61 | 'port' => $this->request->param('port')?? 6379, 62 | 'password' => $this->request->param('password'), 63 | ]; 64 | //测试链接 redis 65 | try{ 66 | $redis = new \Redis(); 67 | $redis->connect($redis_config['host'], $redis_config['port']); 68 | if (!empty($redis_config['password'])) $redis->auth($redis_config['password']); 69 | }catch (\RedisException $e){ 70 | return responseJson(-1 , 'Redis链接失败', $e->getMessage()); 71 | } 72 | return responseJson(1 , 'Redis链接成功'); 73 | } 74 | 75 | public function Install(){ 76 | //检测是否存在安装固定文件 77 | if (file_exists(app()->getRootPath() . 'install.lock')){ 78 | return responseJson(-1 , '系统已安装,如果要重新安装,请删除根目录下的install.lock文件,请注意,重新安装需要清空数据库数据'); 79 | } 80 | $data = [ 81 | 'admin_password' => $this->request->param('admin_password'), 82 | 'requires_key' => $this->request->param('requires_key') ?? 'dynamic', 83 | 'notice_id' => $this->request->param('notice_id') ?? 0, 84 | 'key_last_time' => $this->request->param('key_last_time') ?? 300, 85 | 'fixed_key' => $this->request->param('fixed_key') ?? '', 86 | 'real_url_last_time' => $this->request->param('real_url_last_time') ?? 1800, 87 | 'parse_ua' => $this->request->param('parse_ua') ?? 'netdisk;', 88 | 'normal_cookie' => $this->request->param('normal_cookie') ?? '', 89 | 'is_active' => true 90 | ]; 91 | $db_config = [ 92 | 'hostname' => $this->request->param('db_hostname')?? '127.0.0.1', 93 | 'database' => $this->request->param('db_database')?? 'f4pan', 94 | 'username' => $this->request->param('db_username'), 95 | 'password' => $this->request->param('db_password'), 96 | 'hostport' => $this->request->param('db_port')?? 3306, 97 | ]; 98 | try{ 99 | $pdo = new \PDO("mysql:host={$db_config['hostname']};port={$db_config['hostport']};}", $db_config['username'], $db_config['password']); 100 | $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$db_config['database']}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"); 101 | }catch (\PDOException $e){ 102 | return responseJson(-1 , '数据库链接失败', $e->getMessage()); 103 | } 104 | $redis_config = [ 105 | 'host' => $this->request->param('redis_hostname')?? '127.0.0.1', 106 | 'port' => $this->request->param('redis_port')?? 6379, 107 | 'password' => $this->request->param('redis_password'), 108 | ]; 109 | //缺少参数提示 110 | if (empty($db_config['username']) || empty($db_config['password']) || empty($data['admin_password'])){ 111 | return responseJson(-1 , '缺少必要参数'); 112 | } 113 | $db_host = $db_config['hostname']; 114 | $db_name = $db_config['database']; 115 | $db_user = $db_config['username']; 116 | $db_pass = $db_config['password']; 117 | $db_port = $db_config['hostport']; 118 | $redis_host = $redis_config['host']; 119 | $redis_port = $redis_config['port']; 120 | $redis_pass = $redis_config['password']??''; 121 | $env = <<getRootPath().'.env', $env); 140 | //增加config配置以便于初始化数据库 141 | $config = Config::get('database'); 142 | $config['connections']['install_db']['type'] = 'mysql'; 143 | $config['connections']['install_db']['hostname'] = $db_config['hostname']; 144 | $config['connections']['install_db']['database'] = $db_config['database']; 145 | $config['connections']['install_db']['username'] = $db_config['username']; 146 | $config['connections']['install_db']['password'] = $db_config['password']; 147 | $config['connections']['install_db']['hostport'] = $db_config['hostport']; 148 | $config['connections']['install_db']['charset'] = 'utf8'; 149 | $config['connections']['install_db']['prefix'] = ''; 150 | Config::set($config, 'database'); 151 | $this->initDb($data); 152 | //创建一个安装固定文件 153 | file_put_contents(app()->getRootPath().'install.lock', 'installed'); 154 | return responseJson(1 , '安装成功'); 155 | } 156 | 157 | private function initDb($data){ 158 | $sql = file_get_contents(app()->getRootPath() . 'app/database/db.sql'); 159 | $sql_explode = explode(";", $sql); 160 | Db::connect('install_db')->startTrans(); 161 | foreach ($sql_explode as $sql) { 162 | if (!empty($sql)) { 163 | Db::connect('install_db')->execute($sql); 164 | } 165 | } 166 | Db::connect('install_db')->table('system')->save($data); 167 | Db::connect('install_db')->commit(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /app/common.php: -------------------------------------------------------------------------------- 1 | $code, 11 | 'message' => $message, 12 | 'data' => $data 13 | ], 'json'); 14 | } 15 | 16 | function randomKey(string $text = 'f4pan_apikey_') { 17 | $randomLetters = bin2hex(random_bytes(8)); 18 | $apiKey = $text . $randomLetters; 19 | return $apiKey; 20 | } 21 | 22 | function randomNumKey(string $text = 'f4pan_parse_key_'){ 23 | //六位数字 24 | $randomNumbers = mt_rand(100000, 999999); 25 | $parseKey = $text . $randomNumbers; 26 | return $parseKey; 27 | } 28 | 29 | //function getAccessToken($localstate, $id = null){ 30 | // $url = 'http://127.0.0.1:8003/get_access_token';//使用公共的获取服务 31 | // $res = CurlUtils::post($url, $localstate)->obj(true); 32 | // $model = new SvipModel(); 33 | // if($id){ 34 | // $model->updateSvip($id, ['access_token' => $res['access_token'], 'local_state'=>$res['localstate']]); 35 | // } 36 | // return $res; 37 | //} 38 | 39 | function accountStatus(string $cookie, $localstate=null){ 40 | $url = "https://pan.baidu.com/api/gettemplatevariable?channel=chunlei&web=1&app_id=250528&clienttype=0&fields=[%22username%22,%22loginstate%22,%22is_vip%22,%22is_svip%22,%22is_evip%22]"; 41 | $ua = "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; OPPO R9s Plus Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36"; 42 | $result = getUrlCurl($url, $ua, $cookie); 43 | if($result['errno'] == -6){ 44 | return false; 45 | } 46 | if($result['result']['is_svip']){ 47 | $url_ = "https://pan.baidu.com/rest/2.0/membership/user?method=query&clienttype=0&app_id=250528&web=1"; 48 | $end_time = getUrlCurl($url_, $ua, $cookie)['product_infos']; 49 | foreach ($end_time as $item){ 50 | if($item['detail_cluster'] == 'svip'){ 51 | $end_time = $item; 52 | break; 53 | } 54 | } 55 | $end_time = $end_time['end_time']; 56 | // if ($localstate){ 57 | // $access = ['access_token'=>getAccessToken($localstate)['access_token']]; 58 | // return $result['result']+['end_time'=>$end_time]+$access; 59 | // } 60 | return $result['result']+['end_time'=>$end_time]; 61 | } 62 | return $result['result']+['end_time'=>0]; 63 | } 64 | 65 | function formatSize(float $size, int $times = 0) 66 | { 67 | if ($size > 1024) { 68 | $size /= 1024; 69 | return formatSize($size, $times + 1); // 递归处理 70 | } else { 71 | switch ($times) { 72 | case '0': 73 | $unit = ($size == 1) ? 'Byte' : 'Bytes'; 74 | break; 75 | case '1': 76 | $unit = 'KB'; 77 | break; 78 | case '2': 79 | $unit = 'MB'; 80 | break; 81 | case '3': 82 | $unit = 'GB'; 83 | break; 84 | case '4': 85 | $unit = 'TB'; 86 | break; 87 | case '5': 88 | $unit = 'PB'; 89 | break; 90 | case '6': 91 | $unit = 'EB'; 92 | break; 93 | case '7': 94 | $unit = 'ZB'; 95 | break; 96 | default: 97 | $unit = '单位未知'; 98 | } 99 | return sprintf('%.2f', $size) . $unit; 100 | } 101 | } 102 | 103 | function getUrlCurl($url, $ua, $cookie){ 104 | $curl = curl_init(); 105 | curl_setopt_array($curl, [ 106 | CURLOPT_URL => $url, 107 | CURLOPT_RETURNTRANSFER => true, 108 | CURLOPT_ENCODING => '', 109 | CURLOPT_MAXREDIRS => 10, 110 | CURLOPT_TIMEOUT => 30, 111 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, 112 | CURLOPT_CUSTOMREQUEST => 'GET', 113 | CURLOPT_COOKIE => $cookie, 114 | CURLOPT_HTTPHEADER => [ 115 | 'User-Agent: '.$ua, 116 | 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 117 | 'Accept-Encoding: gzip, deflate, br, zstd', 118 | 'Cache-Control: max-age=0', 119 | 'sec-ch-ua: "Microsoft Edge";v="125", "Chromium";v="125", "Not.A/Brand";v="24"', 120 | 'sec-ch-ua-mobile: ?0', 121 | 'sec-ch-ua-platform: "Windows"', 122 | 'Upgrade-Insecure-Requests: 1', 123 | 'Sec-Fetch-Site: none', 124 | 'Sec-Fetch-Mode: navigate', 125 | 'Sec-Fetch-User: ?1', 126 | 'Sec-Fetch-Dest: document', 127 | 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 128 | ], 129 | ]); 130 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 131 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); 132 | $response = json_decode(curl_exec($curl), true); 133 | curl_close($curl); 134 | return $response; 135 | } 136 | 137 | function postUrlCurl($url, $ua, $cookie, $postData, $headers = [], $referer = '') { 138 | $curl = curl_init(); 139 | 140 | // 合并默认 headers 和用户自定义 headers(用户优先级更高) 141 | $defaultHeaders = [ 142 | 'User-Agent' => $ua, 143 | 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 144 | 'Accept-Encoding' => 'gzip, deflate, br, zstd', 145 | 'Cache-Control' => 'max-age=0', 146 | 'sec-ch-ua' => '"Microsoft Edge";v="125", "Chromium";v="125", "Not.A/Brand";v="24"', 147 | 'sec-ch-ua-mobile' => '?0', 148 | 'sec-ch-ua-platform' => '"Windows"', 149 | 'Upgrade-Insecure-Requests' => '1', 150 | 'Sec-Fetch-Site' => 'none', 151 | 'Sec-Fetch-Mode' => 'navigate', 152 | 'Sec-Fetch-User' => '?1', 153 | 'Sec-Fetch-Dest' => 'document', 154 | 'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 155 | ]; 156 | $headers = array_merge($defaultHeaders, $headers); 157 | 158 | // 处理 Referer 159 | if (!empty($referer) && !isset($headers['Referer'])) { 160 | $headers['Referer'] = $referer; 161 | } 162 | 163 | // 自动处理 POST 数据格式 164 | $contentType = isset($headers['Content-Type']) ? $headers['Content-Type'] : ''; 165 | if (is_array($postData)) { 166 | if (stripos($contentType, 'json') !== false) { 167 | $postData = json_encode($postData); // 数组转 JSON 168 | } else { 169 | $postData = http_build_query($postData); // 数组转 kv 170 | if (empty($contentType)) { 171 | $headers['Content-Type'] = 'application/x-www-form-urlencoded'; // 默认类型 172 | } 173 | } 174 | } else { 175 | // 字符串数据自动检测 JSON 176 | if (empty($contentType)) { 177 | json_decode($postData); 178 | $headers['Content-Type'] = (json_last_error() === JSON_ERROR_NONE) 179 | ? 'application/json' 180 | : 'application/x-www-form-urlencoded'; 181 | } 182 | } 183 | 184 | // 转换 headers 为 cURL 格式 185 | $formattedHeaders = []; 186 | foreach ($headers as $key => $value) { 187 | $formattedHeaders[] = "$key: $value"; 188 | } 189 | 190 | curl_setopt_array($curl, [ 191 | CURLOPT_URL => $url, 192 | CURLOPT_RETURNTRANSFER => true, 193 | CURLOPT_ENCODING => '', 194 | CURLOPT_MAXREDIRS => 10, 195 | CURLOPT_TIMEOUT => 30, 196 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, 197 | CURLOPT_CUSTOMREQUEST => 'POST', 198 | CURLOPT_POSTFIELDS => $postData, 199 | CURLOPT_COOKIE => $cookie, 200 | CURLOPT_HTTPHEADER => $formattedHeaders, 201 | CURLOPT_SSL_VERIFYPEER => false, 202 | CURLOPT_SSL_VERIFYHOST => false, 203 | // CURLOPT_PROXY => '127.0.0.1', 204 | // CURLOPT_PROXYPORT => 7890, 205 | ]); 206 | 207 | $response = json_decode(curl_exec($curl), true); 208 | curl_close($curl); 209 | return $response; 210 | } 211 | 212 | function is_true($val, $return_null=false){ 213 | $boolval = ( is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : (bool) $val ); 214 | return ( $boolval===null && !$return_null ? false : $boolval ); 215 | } -------------------------------------------------------------------------------- /app/controller/Admin.php: -------------------------------------------------------------------------------- 1 | request->param("pwd"); 17 | $model = new SystemModel(); 18 | $_pwd = $model->getAchieve()->toArray()[0]['admin_password']; 19 | if(empty($pwd)){ 20 | return responseJson(-1 , '请输入密码'); 21 | } 22 | if($pwd !== $_pwd){ 23 | return responseJson(-1 , '登录失败'); 24 | }else{ 25 | $token = JWTUtils::getToken([ 26 | 'admin_login'=>true, 27 | 'system_id'=>$model->getAchieve()->toArray()[0]['id'], 28 | ]); 29 | return responseJson(1 , '登录成功' , ['token'=>$token]); 30 | } 31 | } 32 | 33 | public function generateApiKey(){ 34 | $model = new ApiKeyModel(); 35 | $key = randomKey(); 36 | $status = $model->addApiKey(['key'=>$key]); 37 | if (!$status){ 38 | return responseJson(-1 , '生成失败'); 39 | } 40 | return responseJson(1 , '生成成功' , ['key'=>$key]); 41 | } 42 | 43 | public function deleteApiKey(){ 44 | $model = new ApiKeyModel(); 45 | $id = $this->request->param('id'); 46 | if (empty($id)){ 47 | return responseJson(-1 , 'id不能为空'); 48 | } 49 | $status = $model->deleteApiKey($id); 50 | if(!$status){ 51 | return responseJson(-1, "不存在或删除失败"); 52 | } 53 | return responseJson(1 , '删除成功'); 54 | } 55 | 56 | public function getApiKey(){ 57 | $model = new ApiKeyModel(); 58 | $data = $model->getAllApiKeys(); 59 | return responseJson(1 , '获取成功' , $data); 60 | } 61 | 62 | public function addSvip(){ 63 | $model = new SvipModel(); 64 | $cookie = $this->request->param('cookie'); 65 | if(empty($cookie)){ 66 | return responseJson(-1 , 'cookie不能为空'); 67 | } 68 | $account = accountStatus($cookie); 69 | if(!$account){ 70 | return responseJson(-1 , 'cookie失效'); 71 | } 72 | $model->addSvip([ 73 | 'name'=>$account['username'], 74 | 'state'=>0, 75 | 'cookie'=>$cookie, 76 | 'add_time'=>time(), 77 | 'svip_end_time'=>$account['end_time'], 78 | 'vip_type'=>$account['is_vip'] ? '普通用户' : ($account['is_svip'] ? '超级会员' : ($account['is_evip'] ? 'EVIP' : '普通会员')) 79 | ]); 80 | unset($account['access_token']); 81 | return responseJson(1,"添.加.了.",$account); 82 | } 83 | 84 | public function deleteSvip(){ 85 | $model = new SvipModel(); 86 | $id = $this->request->param('id'); 87 | if(empty($id)){ 88 | return responseJson(-1 , 'id不能为空'); 89 | } 90 | $status = $model->deleteSvip($id); 91 | if(!$status){ 92 | return responseJson(-1,"不.存.在"); 93 | } 94 | return responseJson(1,"删.除.了."); 95 | } 96 | 97 | public function updateSvip(){ 98 | $model = new SvipModel(); 99 | $id = $this->request->param('id'); 100 | if(empty($id)){ 101 | return responseJson(-1 , 'id不能为空'); 102 | } 103 | $state = $model->getSvipById($id)->toArray(); 104 | $state = $state["state"] == 0 ? -1 : 0; 105 | $status = $model->updateSvip($id , [ 106 | 'state'=>$state 107 | ]); 108 | if(!$status){ 109 | return responseJson(-1,"不.存.在"); 110 | } 111 | return responseJson(1,"更.新.了."); 112 | } 113 | 114 | public function getSvipList(){ 115 | $model = new SvipModel(); 116 | $data = $model->getAllNormalSvips(); 117 | return responseJson(1 , '获取成功' , $data); 118 | } 119 | 120 | public function getAllList(){ 121 | $model = new SvipModel(); 122 | $data = $model->getAllList(); 123 | foreach ($data as $k=>$va){ 124 | $can_use = true; 125 | if($va['state'] != 0 || $va['svip_end_time'] < time()){ 126 | $can_use = false; 127 | } 128 | $va['can_use'] = $can_use; 129 | $va['show_msg'] = $can_use ? '可用' : ($va['state'] != 0 ? "限速" : '过期'); 130 | } 131 | return responseJson(1 , '获取成功' , $data); 132 | } 133 | 134 | public function getAllNotice(){ 135 | $model = new NoticeModel(); 136 | return responseJson(1 , '获取成功' , $model->getAllNotice()); 137 | } 138 | 139 | public function addNotice(){ 140 | $model = new NoticeModel(); 141 | $data = [ 142 | 'title'=>$this->request->param('title'), 143 | 'content'=>$this->request->param('content'), 144 | 'add_time'=>time() 145 | ]; 146 | $status = $model->addNotice($data); 147 | if(!$status){ 148 | return responseJson(-1,"添.加.失.败"); 149 | } 150 | return responseJson(1,"添.加.了.", $data); 151 | } 152 | 153 | public function updateNotice(){ 154 | $model = new NoticeModel(); 155 | $id = $this->request->param('id'); 156 | if(empty($id)){ 157 | return responseJson(-1 , 'id不能为空'); 158 | } 159 | $data = [ 160 | 'title'=>$this->request->param('title'), 161 | 'content'=>$this->request->param('content'), 162 | ]; 163 | $status = $model->updateNotice($id , $data); 164 | if(!$status){ 165 | return responseJson(-1,"不.存.在"); 166 | } 167 | return responseJson(1,"更.新.了.", $data); 168 | } 169 | 170 | public function deleteNotice(){ 171 | $model = new NoticeModel(); 172 | $id = $this->request->param('id'); 173 | if(empty($id)){ 174 | return responseJson(-1 , 'id不能为空'); 175 | } 176 | $status = $model->deleteNotice($id); 177 | if(!$status){ 178 | return responseJson(-1,"不.存.在"); 179 | } 180 | return responseJson(1,"删.除.了."); 181 | } 182 | 183 | public function useNotice(){ 184 | $model = new SystemModel(); 185 | $notice_model = new NoticeModel(); 186 | $id = $this->request->param('id'); 187 | if(empty($id)){ 188 | return responseJson(-1 , 'id不能为空'); 189 | } 190 | if(!$notice_model->existNotice($id)){ 191 | return responseJson(-1,"不.存.在"); 192 | } 193 | $status = $model->updateSystem($model->getAchieve()->toArray()[0]['id'],['notice_id'=>$id]); 194 | if(!$status){ 195 | return responseJson(-1,"出.错.了"); 196 | } 197 | return responseJson(1,"使.用.了."); 198 | } 199 | 200 | public function getAllSystem(){ 201 | $model = new SystemModel(); 202 | return responseJson(1 , '获取成功' , $model->getAllSystem()->toArray()); 203 | } 204 | 205 | public function addSystem(){ 206 | $model = new SystemModel(); 207 | $isActiveExists = $model->where('is_active', true)->count() > 0; 208 | $data = [ 209 | 'admin_password' => $this->request->param('admin_password'), 210 | 'requires_key' => $this->request->param('requires_key') ?? 'dynamic', 211 | 'notice_id' => $this->request->param('notice_id') ?? 0, 212 | 'key_last_time' => $this->request->param('key_last_time') ?? 300, 213 | 'fixed_key' => $this->request->param('fixed_key') ?? '', 214 | 'real_url_last_time' => $this->request->param('real_url_last_time') ?? 1800, 215 | 'parse_ua' => $this->request->param('parse_ua') ?? 'netdisk', 216 | 'normal_cookie' => $this->request->param('normal_cookie') ?? '', 217 | 'is_active' => !$isActiveExists // 如果有记录为true,则新记录为false;反之为true 218 | ]; 219 | $status = $model->save($data); 220 | if (!$status) { 221 | return responseJson(-1, "添.加.失.败"); 222 | } 223 | $id = $model->id; 224 | return responseJson(1, "添.加.了.", ["id"=>$id]+$data); 225 | } 226 | 227 | public function updateSystem(){ 228 | $model = new SystemModel(); 229 | $id = $this->request->param('id'); 230 | if(empty($id)){ 231 | return responseJson(-1 , 'id不能为空'); 232 | } 233 | $data = [ 234 | 'admin_password' => $this->request->param('admin_password'), 235 | 'requires_key' => $this->request->param('requires_key') ?? 'dynamic', 236 | 'notice_id' => $this->request->param('notice_id') ?? 0, 237 | 'key_last_time' => $this->request->param('key_last_time') ?? 300, 238 | 'fixed_key' => $this->request->param('fixed_key') ?? '', 239 | 'real_url_last_time' => $this->request->param('real_url_last_time') ?? 1800, 240 | 'parse_ua' => $this->request->param('parse_ua') ?? 'netdisk', 241 | 'normal_cookie' => $this->request->param('normal_cookie') ?? '', 242 | ]; 243 | $status = $model->updateSystem($id , $data); 244 | if(!$status){ 245 | return responseJson(-1,"没有变更内容"); 246 | } 247 | return responseJson(1,"更.新.了.", $data); 248 | } 249 | 250 | public function deleteSystem(){ 251 | $model = new SystemModel(); 252 | $id = $this->request->param('id'); 253 | if(empty($id)){ 254 | return responseJson(-1 , 'id不能为空'); 255 | } 256 | $status = $model->deleteSystem($id); 257 | if(!$status){ 258 | return responseJson(-1,"不.存.在"); 259 | } 260 | return responseJson(1,"删.除.了."); 261 | } 262 | 263 | public function useSystem(){ 264 | $model = new SystemModel(); 265 | $id = $this->request->param('id'); 266 | if(empty($id)){ 267 | return responseJson(-1 , 'id不能为空'); 268 | } 269 | $status = $model->useSystemById($id); 270 | if(!$status){ 271 | return responseJson(-1,"出.错.了"); 272 | } 273 | return responseJson(1,"使.用.了."); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /app/utils/CurlUtils.php: -------------------------------------------------------------------------------- 1 | cookie_jar = mb_convert_encoding(__DIR__, "GBK", "UTF-8") . DIRECTORY_SEPARATOR . 'cookie.txt'; 19 | $this->options= [ 20 | CURLOPT_HEADER => true, // 输出响应头 21 | CURLOPT_TIMEOUT=> 5,// 超时 秒 22 | CURLOPT_RETURNTRANSFER => true, // 输出数据流 23 | CURLOPT_FOLLOWLOCATION => true, // 自动跳转追踪 24 | CURLOPT_AUTOREFERER=> true, // 自动设置来路信息 25 | CURLOPT_SSL_VERIFYPEER => false,// 认证证书检查 26 | CURLOPT_SSL_VERIFYHOST => false,// 检查SSL加密算法 27 | CURLOPT_NOSIGNAL => true, // 忽略所有传递的信号 28 | CURLOPT_HTTPHEADER => [], // 请求头 29 | CURLINFO_HEADER_OUT=> true, // 获取请求头 30 | CURLOPT_ENCODING => 'gzip' 31 | ]; 32 | } 33 | public static function __callStatic ($method, $args) { 34 | if (self::$instance === null) 35 | self::$instance = new self(); 36 | return self::$instance->$method(...$args); 37 | } 38 | public function __call ($method, $args) { 39 | switch ($method) { 40 | case 'autoCookie': // Cookie自动化,默认关闭 41 | if (@$args[0]) { 42 | $this->options[CURLOPT_COOKIEFILE] = $this->cookie_jar; 43 | $this->options[CURLOPT_COOKIEJAR]= $this->cookie_jar; 44 | } 45 | break; 46 | case 'timeout': 47 | if (isset($args[0])) 48 | $this->options[CURLOPT_TIMEOUT] = $args[0]; 49 | break; 50 | case 'location': // 自动重定向,默认开启 51 | if (isset($args[0])) 52 | $this->options[CURLOPT_FOLLOWLOCATION] = $args[0]; 53 | break; 54 | case 'get': 55 | if (isset($args[0])) { 56 | $data = isset($args[1]) ? is_array($args[1]) ? http_build_query($args[1], '', '&', PHP_QUERY_RFC3986) : $args[1] : false; 57 | $this->options[CURLOPT_URL] = $data !== false ? $args[0] . (preg_match('/\?/', $args[0]) ? '&' : '?') . $data : $args[0]; 58 | } 59 | break; 60 | case 'upload': 61 | if (isset($args[0])) { 62 | $this->options[CURLOPT_URL]= $args[0]; 63 | $this->options[CURLOPT_POST] = true; 64 | if (isset($args[1])) 65 | $this->options[CURLOPT_POSTFIELDS] = $args[1]; 66 | } 67 | break; 68 | case 'post': 69 | if (isset($args[0])) { 70 | $this->options[CURLOPT_URL]= $args[0]; 71 | $this->options[CURLOPT_POST] = true; 72 | if (isset($args[1])) 73 | $this->options[CURLOPT_POSTFIELDS] = is_array($args[1]) ? http_build_query($args[1], '', '&', PHP_QUERY_RFC3986) : $args[1]; 74 | } 75 | break; 76 | case 'json': 77 | if (isset($args[0])) { 78 | $this->options[CURLOPT_URL]= $args[0]; 79 | $this->options[CURLOPT_POST] = true; 80 | $this->options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json; charset=utf-8'; 81 | if (!empty(@$args[1])) 82 | $this->options[CURLOPT_POSTFIELDS] = is_array($args[1]) ? json_encode($args[1], 256) : $args[1]; 83 | } 84 | break; 85 | case 'ua': 86 | if (isset($args[0])) { 87 | if (in_array(strtolower($args[0]), ['m', 'wap', 'android'])) 88 | $args[0] = self::ua_android; else if (strtolower($args[0]) === 'pc') 89 | $args[0] = self::ua_pc; else if (strtolower($args[0]) === 'audo') 90 | $args[0] = @$_SERVER['HTTP_USER_AGENT']; 91 | $this->options[CURLOPT_USERAGENT] = $args[0]; 92 | } 93 | break; 94 | case 'referer': 95 | if (isset($args[0])) 96 | $this->options[CURLOPT_REFERER] = $args[0] === true && !empty(@$this->options[CURLOPT_URL]) ? $this->options[CURLOPT_URL] : $args[0]; 97 | break; 98 | case 'header': // 来路 99 | if (isset($args[0])) { 100 | if (is_array($args[0])) { 101 | // 数组键值对形式 102 | foreach ($args[0] as $k => $v) 103 | $this->options[CURLOPT_HTTPHEADER][] = $k . ': ' . $v; 104 | } else if (isset($args[1])) { 105 | // 双参数键值形式 106 | $this->options[CURLOPT_HTTPHEADER][] = $args[0] . ': ' . $args[1]; 107 | } else { 108 | // 单参数形式 109 | $arr = preg_match_all('/([^:]*?)\s*:\s*(.*)\s*/', $args[0], $m) > 0 ? array_combine($m[1], $m[2]) : []; 110 | foreach ($arr as $k => $v) 111 | $this->options[CURLOPT_HTTPHEADER][] = $k . ': ' . $v; 112 | } 113 | } 114 | break; 115 | case 'cookie': 116 | if (isset($args[0])) { 117 | if (is_array($args[0])) { 118 | // 数组键值对形式 119 | $arr = []; 120 | foreach ($args[0] as $k => $v) 121 | $arr[] = $k . '=' . $v; 122 | $cookie = implode('; ', $arr); 123 | } else if (isset($args[1])) // 双参数键值形式 124 | $cookie = $args[0] . '=' . $args[1] . ';'; else // 单参数形式 125 | $cookie = $args[0]; 126 | $this->options[CURLOPT_COOKIE] = !empty(@$this->options[CURLOPT_COOKIE]) ? $this->options[CURLOPT_COOKIE] 127 | . (preg_match('/;\s*$/', $this->options[CURLOPT_COOKIE]) ? '' : '; ') . $cookie : $cookie; 128 | } 129 | break; 130 | case 'ajax': 131 | if (isset($args[0]) && $args[0] === true) 132 | $this->options[CURLOPT_HTTPHEADER][] = 'X-Requested-With: XMLHttpRequest'; 133 | break; 134 | case 'getCookie': // 读取Cookie文件中的cookie 135 | $url= isset($args[0]) && filter_var($args[0], FILTER_VALIDATE_URL) ? $args[0] : $this->options[CURLOPT_URL]; 136 | $cookie = @file_get_contents($this->cookie_jar); 137 | $arr= parse_url($url); 138 | $host = preg_replace('/.*?\.(?=.+\..+$)/', '', $arr['host']); 139 | $p= preg_match_all('/' . preg_quote($host, '/') . '(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\r*\n|$)/', rawurldecode($cookie), $m); 140 | return $p > 0 ? array_combine($m[5], $m[6]) : []; 141 | case 'head': // 获取响应头,默认不需要响应体,false表示需要 142 | $this->options[CURLOPT_HEADER] = true; 143 | $this->options[CURLOPT_NOBODY] = true; 144 | return $this->request($method, $args); 145 | case 'html': 146 | case 'obj': 147 | case 'xml': 148 | case 'text': 149 | case 'body': 150 | case 'arrayobject': 151 | case 'jsonobject': 152 | case 'response': 153 | case 'all': 154 | return $this->request($method, $args); 155 | case 'down': 156 | $this->options[CURLOPT_HEADER] = false; 157 | $this->options[CURLOPT_NOBODY] = false; 158 | $this->options[CURLOPT_FOLLOWLOCATION] = true; 159 | return $this->request($method, $args); 160 | default: 161 | exit("错误:不支持{$method}方法!"); 162 | } 163 | return $this; 164 | } 165 | //执行请求(get, post)并返回数据 166 | private function request ($method, $args) { 167 | $ch = curl_init(); 168 | curl_setopt_array($ch, $this->options); 169 | $data = curl_exec($ch); 170 | $info = curl_getinfo($ch); 171 | curl_close($ch); 172 | if ($method === 'down') { 173 | if (isset($args[1])) $file = trim($args[0]) . DIRECTORY_SEPARATOR . trim($args[1]); else if (isset($args[0])) $file = trim($args[0]); else $file = './' . pathinfo($this->options[CURLOPT_URL], PATHINFO_BASENAME); 174 | try { 175 | $fo = fopen($file, 'a'); 176 | fwrite($fo, $data); 177 | fclose($fo); 178 | return true; 179 | } 180 | catch (\Exception $e) { 181 | return $e->getMessage(); 182 | } 183 | } 184 | $info['header'] = trim(substr($data, 0, $info['header_size'])); 185 | $info['response'] = substr($data, $info['header_size']); 186 | $headers = [$info['http_code']]; 187 | $p = preg_match_all('/(?<=^|[\r\n])\s*([^:]+)\s*:\s*(.+?)\s*(?=[\r\n]|$)/', $info['header'], $n); 188 | $info['headers'] = $p > 0 ? array_merge_recursive($headers, $this->array_combine_recursive($n[1], $n[2])) : []; 189 | $cookies = isset($info['headers']['Set-Cookie']) ? $info['headers']['Set-Cookie'] : @$info['headers']['set-cookie']; 190 | $info['cookies'] = !empty($cookies) ? $this->parse_cookie(is_array($cookies) ? $cookies : [$cookies]) : []; 191 | $info['cookie']= !empty($info['cookies']) ? array_column($info['cookies'], 'value', 'key') : []; 192 | $info['obj'] = ($obj = json_decode($info['response'])) && is_object($obj) ? $obj : null; 193 | $result = $info['response']; 194 | switch ($method) { 195 | case 'head': 196 | $result = $info[@$args[0] ? 'header' : 'headers']; 197 | break; 198 | case 'getCookie': 199 | $result = $info['cookie']; 200 | break; 201 | case 'text': 202 | case 'body': 203 | case 'response': 204 | case 'html': 205 | $result = $info['response']; 206 | if (isset($args[0])) 207 | $result = mb_convert_encoding($result, 'UTF-8', $args[0]); 208 | if ($method === 'html') 209 | $result = html_entity_decode($result); 210 | break; 211 | case 'obj': 212 | $result = json_decode($info['response'], @$args[0] === true); 213 | break; 214 | case 'jsonobject': 215 | case 'arrayobject': 216 | $result = json_decode($info['response'],true); 217 | break; 218 | case 'xml': 219 | $result = simplexml_load_string($info['response'], 'SimpleXMLElement', LIBXML_NOCDATA); 220 | break; 221 | case 'all': 222 | $result = isset($args[0]) ? @$info[$args[0]] : $info; 223 | break; 224 | } 225 | self::$instance = null; 226 | return $result; 227 | } 228 | private function array_combine_recursive ($keys, $values) { 229 | $arr = []; 230 | foreach ($keys as $i => $key) { 231 | $value = isset($values[$i]) ? $values[$i] : null; 232 | if (!isset($arr[$key])) 233 | $arr[$key] = $value; else if (is_array($arr[$key])) 234 | $arr[$key][] = $value; else 235 | $arr[$key] = [$arr[$key], $value]; 236 | } 237 | return $arr; 238 | } 239 | private function parse_cookie ($header) { 240 | $parse_cookie_str = function ($str) { 241 | $p= preg_match_all('/(?<=^|;)\s*([^=]+?)\s*=\s*(.*?)\s*(?=;|$)/', $str, $m); 242 | $i= 0; 243 | $cookie = []; 244 | while ($i < $p) { 245 | if ($i === 0) { 246 | $cookie['key'] = $m[1][$i]; 247 | $cookie['value'] = $m[2][$i]; 248 | } else if ($m[1][$i] === 'expires') { 249 | $d = date_create($m[2][$i], timezone_open("Asia/Shanghai")); 250 | $timestamp = $d->getTimestamp(); 251 | $cookie[$m[1][$i]] = $timestamp; 252 | } else 253 | $cookie[$m[1][$i]] = $m[2][$i]; 254 | $i++; 255 | } 256 | return $cookie; 257 | }; 258 | $cookies = []; 259 | if (is_array($header)) { 260 | foreach ($header as $cookie) { 261 | $cookie = $parse_cookie_str($cookie); 262 | if (!empty($cookie)) 263 | $cookies[$cookie['key']] = $cookie; 264 | } 265 | } else if (is_string($header)) { 266 | $p = preg_match_all('/(?<=^|[\r\n])\s*Set-Cookie\s*:\s*(.+?)(?=[\r\n]|$)/i', $header, $m); 267 | $i = 0; 268 | while ($i < $p) { 269 | $cookie = $parse_cookie_str($m[1][$i]); 270 | if (!empty($cookie)) 271 | $cookies[$cookie['key']] = $cookie; 272 | $i++; 273 | } 274 | } 275 | return $cookies; 276 | } 277 | } -------------------------------------------------------------------------------- /app/controller/Parse.php: -------------------------------------------------------------------------------- 1 | getAllNormalSvips()->toArray(); 24 | if (empty($Svips)) { 25 | $Svips = $SvipModel->getAllList()->toArray(); 26 | } 27 | if (empty($Svips)) { 28 | return false; 29 | } 30 | $randIndex = array_rand($Svips); 31 | $Svip = $Svips[$randIndex]; 32 | 33 | $Svip_cookie = $Svip['cookie']; 34 | $Svip_id = $Svip['id']; 35 | $info = accountStatus($Svip_cookie); 36 | if ($info) { 37 | return [$Svip_cookie, $Svip_id]; 38 | } else { 39 | $SvipModel->updateSvip($Svip_id, ['state' => -1]); 40 | (new StatsModel())->addSpentSvipCount(); 41 | return false; 42 | } 43 | } 44 | 45 | 46 | private function getSign($share_id, $uk){ 47 | $tplconfig = "https://pan.baidu.com/share/tplconfig?shareid={$share_id}&uk={$uk}&fields=sign,timestamp&channel=chunlei&web=1&app_id=250528&clienttype=0"; 48 | $sign = CurlUtils::cookie(SystemModel::getNormalCookie())->ua(SystemModel::getUa())->get($tplconfig)->obj(true); 49 | return $sign; 50 | } 51 | 52 | public function getFileList() 53 | { 54 | $shorturl = $this->request->surl; 55 | $password = $this->request->password; 56 | $isRoot = $this->request->isroot; 57 | $dir = $this->request->dir; 58 | $url = 'https://pan.baidu.com/share/wxlist?channel=weixin&version=2.2.2&clienttype=25&web=1'; 59 | $root = ($isRoot) ? "1" : "0"; 60 | $dir = urlencode($dir); 61 | $data = "shorturl=$shorturl&dir=$dir&root=$root&pwd=$password&page=1&num=1000&order=time"; 62 | $header = array( 63 | "User-Agent: netdisk", 64 | "Referer: https://pan.baidu.com/disk/home" 65 | ); 66 | $result = CurlUtils::header($header)->cookie(SystemModel::getNormalCookie())->post($url, $data)->obj(true); 67 | if ($result['errno'] != "0"){ 68 | return responseJson(-1, '链接错误,请检查链接是否有效'); 69 | } 70 | $array = []; 71 | foreach ($result['data']['list'] as $va){ 72 | $filename = $va['server_filename']; 73 | $ctime = $va['server_ctime']; 74 | $path = $va['path']; 75 | $md5 = $va['md5']??""; 76 | $fs_id = (int)$va['fs_id']; 77 | $isdir = $va['isdir']; 78 | $size = (int)$va['size']; 79 | $array[] = array('filename'=>$filename,'ctime'=>$ctime,'path'=>$path,'md5'=>$md5,'fs_id'=>$fs_id,'isdir'=>$isdir,'size'=>$size); 80 | } 81 | if(!$array){ 82 | $array = []; 83 | } 84 | $share_id = $result['data']['shareid']; 85 | $uk = $result['data']['uk']; 86 | $seckey = $result['data']['seckey']; 87 | $seckey = str_replace("-", "+", $seckey); 88 | $seckey = str_replace("~", "=", $seckey); 89 | $seckey = str_replace("_", "/", $seckey); 90 | return responseJson(200, "获取成功", array('list'=>$array,'shareinfo'=>array('share_id'=>$share_id,'uk'=>$uk,'seckey'=>$seckey))); 91 | } 92 | 93 | //6.22更新 94 | public function parseFile() 95 | { 96 | $redis = \think\facade\Cache::store('redis'); 97 | $fs_id = $this->request->fs_id; 98 | $randsk = $this->request->randsk; 99 | $share_id = $this->request->share_id; 100 | $uk = $this->request->uk; 101 | $surl = $this->request->surl; 102 | $short = $this->request->short; 103 | if ( 104 | empty($fs_id) || 105 | empty($surl) || 106 | empty($randsk) || 107 | empty($share_id) || 108 | empty($uk) 109 | ) { 110 | return responseJson(-1, "缺少必要参数"); 111 | } 112 | if($redis->get('parse_'.$fs_id)){ 113 | $result = json_decode($redis->get('parse_'.$fs_id),true); 114 | $result['use_cache'] = true; 115 | return responseJson(200, "获取成功", $result); 116 | } 117 | $cookie = $this->getRandomSvipCookie(); 118 | if (!$cookie){ 119 | return responseJson(-1, "获取可用账号失败"); 120 | } 121 | if(!self::checkDir($cookie[0])){ 122 | if(!self::createNewDir($cookie[0])){ 123 | return responseJson(-1, "创建文件夹失败"); 124 | }; 125 | } 126 | $url = 'https://pan.baidu.com/s/'.$surl; 127 | $array = self::transfer($cookie,$share_id,$uk,$fs_id,$randsk,$url); 128 | $to_fs_id = $array['to_fs_id']; 129 | $to_path = $array['to_path']; 130 | if (!$to_fs_id){ 131 | $id = $cookie[1]; 132 | $model = new SvipModel(); 133 | $model->updateSvip($id, ['state' =>-1]); 134 | $model_ = new StatsModel(); 135 | $model_->addSpentSvipCount(); 136 | $cookie = $this->getRandomSvipCookie(); 137 | if(!$cookie){ 138 | return responseJson(-1, "获取可用账号失败"); 139 | } 140 | $cookie = $this->getRandomSvipCookie(); 141 | $array = self::transfer($cookie,$share_id,$uk,$fs_id,$randsk,$url); 142 | $to_fs_id = $array['to_fs_id']; 143 | $to_path = $array['to_path']; 144 | if (!$to_path){ 145 | $model = new SvipModel(); 146 | $model->updateSvip($id, ['state' =>-1]); 147 | return responseJson(-1, "转移文件失败,请检查解析账号可用空间\n暂不支持解析 解析账号分享的文件"); 148 | } 149 | } 150 | $to_path = rawurlencode($to_path); 151 | $url = "https://d.pcs.baidu.com/rest/2.0/pcs/file?ant=1&apn_id=33_13&app_id=250528&channel=0&check_blue=1&clienttype=17&cuid=08E271F7046B366BE1BF9F1F30DF0689%7CVFZVYSRCU&deviceid=611777535803319847&devuid=08E271F7046B366BE1BF9F1F30DF0689%7CVFZVYSRCU&dtype=1&eck=1&ehps=1&err_ver=1.0&es=1&esl=1&freeisp=0&method=locatedownload&network_type=4G&path=$to_path&queryfree=0&rand=0854bec9ad10241680eb16aaf3e9ab3912f0f429&time=1744558717&use=0&ver=4.0&version=2.2.101.242&version_app=12.25.3&vip=0&psign=aa42ffc322b4c71d2f39e422aa83607e"; 152 | $res = getUrlCurl($url, SystemModel::getUa(), $cookie[0]); 153 | if (!isset($res['urls'])) { 154 | $msg = isset($res['errmsg']) ? $res['errmsg'] : '未知错误'; 155 | return responseJson(-1, $msg, $res); 156 | } 157 | $realLink = $res['urls'][0]['url']; 158 | if (str_contains($realLink, "nd6.baidupcs.com") && count($res["urls"]) > 1){ 159 | $realLink = $res['urls'][rand(1, count($res["urls"])-1)]['url']; 160 | } 161 | $realLink .= "&origin=dlna"; 162 | preg_match("/size=(\d+)/", $realLink, $pp); 163 | $filesize = $pp[1]; 164 | preg_match("/&fin=(.+)&bflag/", $realLink, $pp); 165 | $filename = $pp[1]; 166 | $filename = urldecode($filename); 167 | preg_match("/\/file\/(.+)\?/", $realLink, $pp); 168 | $filemd5 = $pp[1]; 169 | preg_match("/ctime=(\d+)/", $realLink, $pp); 170 | $filectime = $pp[1]; 171 | 172 | if ($realLink == "" or str_contains($realLink, "qdall01.baidupcs.com")) { 173 | $model = new SvipModel(); 174 | $model->updateSvip($cookie[1], array('state' => -1)); 175 | $model = new StatsModel(); 176 | $model->addSpentSvipCount(); 177 | // print_r($realLink); 178 | return responseJson(-1, "解析失败,可能账号已限速,请3s后重试,账号ID{$cookie[1]}"); 179 | } 180 | 181 | $realLink = is_true($short) ? self::createShortUrl($realLink, $surl, (int)$fs_id) : $realLink; 182 | $result = array( 183 | 'filename' => $filename, 184 | 'filectime' => $filectime, 185 | 'filemd5' => $filemd5, 186 | 'filefsid' => (int)$fs_id, 187 | 'filesize' => $filesize, 188 | 'dlink' => $realLink, 189 | 'ua' => SystemModel::getUa(), 190 | 'use_cache'=>false 191 | ); 192 | $model = new SystemModel(); 193 | $last_time = $model->getAchieve()->toArray()[0]['real_url_last_time']; 194 | $redis->set('parse_'.$fs_id, json_encode($result), $last_time); 195 | //进入统计 196 | $model = new StatsModel(); 197 | $model->addParsingCount(); 198 | $model->addTraffic($filesize); 199 | //每日统计 200 | $stats = new StatsDailyModel(); 201 | $stats->addTraffic($filesize); 202 | $stats->addParsingCount(); 203 | return responseJson(200, "获取成功", $result); 204 | } 205 | 206 | public function shortUrlRedirect(string $code) 207 | { 208 | $redis = \think\facade\Cache::store('redis'); 209 | $url = $redis->get("short_url_" . $code); 210 | if ($url) { 211 | return redirect($url); 212 | } else { 213 | return responseJson(-1, "短链接不存在或已过期", ['code'=>$code]); 214 | } 215 | } 216 | 217 | private static function createShortUrl(string $url, string $surl, int $fsid): string 218 | { 219 | $redis = \think\facade\Cache::store('redis'); 220 | $shortCode = $surl . ':' . $fsid; 221 | $model = new SystemModel(); 222 | $last_time = $model->getAchieve()->toArray()[0]['real_url_last_time']; 223 | $redis->set("short_url_" . $shortCode, $url, $last_time); 224 | return request()->domain() . '/api/v1/s/' . $shortCode; 225 | } 226 | 227 | public static function checkDir($cookie){ 228 | $url = 'https://pan.baidu.com/api/list?channel=chunlei&bdstoken=e6bc800efaabbc3b1b07952bedc1d445&app_id=250528&dir=%2F&order=name&desc=0&start=0&limit=500&t=0.5963396759604782&channel=chunlei&web=1&bdstoken=e6bc800efaabbc3b1b07952bedc1d445&logid=RENBODQ1MkY3Mzg4MEMzOUUzOTBCQ0JCRDM0NEYwMzY6Rkc9MQ==&clienttype=0&dp-logid=93935300557954940027'; 229 | $ua = "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; OPPO R9s Plus Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36"; 230 | $res = getUrlCurl($url, $ua, $cookie); 231 | foreach ($res['list'] as $k=>$va){ 232 | if($va['path'] == "/parse_file" && $va['isdir'] == 1){ 233 | return true; 234 | } 235 | } 236 | return false; 237 | } 238 | 239 | public static function createNewDir($cookie){ 240 | $url = 'https://pan.baidu.com/api/create?a=commit&channel=chunlei&bdstoken=e6bc800efaabbc3b1b07952bedc1d445&app_id=250528&channel=chunlei&web=1&bdstoken=e6bc800efaabbc3b1b07952bedc1d445&logid=RENBODQ1MkY3Mzg4MEMzOUUzOTBCQ0JCRDM0NEYwMzY6Rkc9MQ==&clienttype=0&dp-logid=25871100140032000048'; 241 | $ua = "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; OPPO R9s Plus Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36"; 242 | //$res = CurlUtils::cookie($cookie)->post($url, 'path=//parse_file&isdir=1&size=&block_list=[]&method=post&dataType=json')->obj(true); 243 | $res = postUrlCurl($url, $ua, $cookie, "path=//parse_file&isdir=1&size=&block_list=[]&method=post&dataType=json"); 244 | if ($res['errno'] == 0){ 245 | return true; 246 | } 247 | return false; 248 | } 249 | 250 | public static function transfer($cookie, $shareid, $from, $fsid, $randsk, $shareurl){ 251 | $ua = "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; OPPO R9s Plus Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36"; 252 | $bdstoken = getUrlCurl("https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=[%22bdstoken%22,%22token%22,%22uk%22,%22isdocuser%22,%22servertime%22]", $ua, $cookie[0]); 253 | $bdstoken = $bdstoken['result']['bdstoken']; 254 | $randsk = urlencode($randsk); 255 | $cookie[0] .= ";BDCLND=$randsk"; 256 | //curl 257 | $curl = curl_init(); 258 | 259 | curl_setopt_array($curl, [ 260 | CURLOPT_URL => "https://pan.baidu.com/share/transfer?shareid=$shareid&from=$from&sekey=$randsk&ondup=newcopy&async=1&channel=chunlei&web=1&app_id=250528&bdstoken=$bdstoken&logid=N0Y0NTVBMDg1NkZFNDVFMjVEQzYxMUE0OUUwMEM5QzM6Rkc9MQ%3D%3D&clienttype=0&dp-logid=48806300903566960041", 261 | CURLOPT_RETURNTRANSFER => true, 262 | CURLOPT_ENCODING => '', 263 | CURLOPT_MAXREDIRS => 10, 264 | CURLOPT_TIMEOUT => 30, 265 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, 266 | CURLOPT_CUSTOMREQUEST => 'POST', 267 | CURLOPT_POSTFIELDS => "fsidlist=[$fsid]&path=/parse_file&type=1'", 268 | CURLOPT_COOKIE => $cookie[0], 269 | CURLOPT_HTTPHEADER => [ 270 | 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36', 271 | 'Accept: application/json, text/javascript, */*; q=0.01', 272 | 'Accept-Encoding: gzip, deflate, br, zstd', 273 | 'Content-Type: application/x-www-form-urlencoded', 274 | 'sec-ch-ua-platform: "Android"', 275 | 'X-Requested-With: XMLHttpRequest', 276 | 'sec-ch-ua: "Android WebView";v="135", "Not-A.Brand";v="8", "Chromium";v="135"', 277 | 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', 278 | 'sec-ch-ua-mobile: ?1', 279 | 'Origin: https://pan.baidu.com', 280 | 'Sec-Fetch-Site: same-origin', 281 | 'Sec-Fetch-Mode: cors', 282 | 'Sec-Fetch-Dest: empty', 283 | 'Referer: '.$shareurl, 284 | 'Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 285 | ], 286 | ]); 287 | 288 | $res = json_decode(curl_exec($curl), true); 289 | curl_close($curl); 290 | //print_r($res); 291 | if($res['errno'] == 9013){ 292 | $model = new SvipModel(); 293 | $model->updateSvip($cookie[1], array('state' => -1)); 294 | return array('to_path'=>null,'to_fs_id'=>null,'cookie'=>$cookie);; 295 | } 296 | if($res['errno'] == 12){ 297 | $model = new SvipModel(); 298 | $model->updateSvip($cookie[1], array('state' => -1)); 299 | return array('to_path'=>null,'to_fs_id'=>null,'cookie'=>$cookie);; 300 | } 301 | if($res['errno'] == 2){ 302 | return array('to_path'=>null,'to_fs_id'=>null,'cookie'=>$cookie);; 303 | } 304 | $to_path = $res['extra']['list'][0]['to']; 305 | $to_fs_id = $res['extra']['list'][0]['to_fs_id']; 306 | return array('to_path'=>$to_path,'to_fs_id'=>$to_fs_id,'cookie'=>$cookie); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------