PHP: Guzzle 6 - ошибка cURL 7 Соединение отклонено

Я искал, искал и читал документацию по адресу http://docs.guzzlephp.org/en/stable/request-options.html и подтвердил ошибку на странице https://curl.haxx.se/libcurl/c/libcurl-errors.html и хоть убей, я не могу понять, что происходит. У меня есть URL-адреса для приложения-один и приложения-два в моем файле /etc/hosts, и я знаю, что они верны, поскольку я могу получить к ним доступ в своем браузере и с помощью cURL через терминал.

Моя установка:

Контейнеры Docker настроены как:

App 1 = php-fpm - responding app
App 2 = php-fpm - requesting app, using Guzzle 6.3.2
Nginx Reverse Proxy

конфигурации нгинкс:

Приложение 1:

upstream php-app-one {
    server php-app-one:9000;
}

server {
    listen 80;
    listen [::]:80;
    server_name app-one.local;
    return 301 https://$server_name$request_uri;
}

server {
    # SSL configuration
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl on;
    ssl_certificate /etc/nginx/certs/app-one.crt;
    ssl_certificate_key /etc/nginx/certs/app-one.key;
    ssl_dhparam /etc/nginx/certs/dhparam.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    server_name app-one.local;

    root /var/www/app-one;
    index index.php index.html;

    gzip_types text/plain text/css application/json application/x-javascript
               text/xml application/xml application/xml+rss text/javascript;

    # Add headers to serve security related headers
    #
    # Disable preloading HSTS for now.  You can use the commented out header line that includes
    # the "preload" directive if you understand the implications.
    # add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header Pragma "no-cache";
    add_header Cache-Control "no-cache";
    add_header X-uri "$uri";

    location ~* \.(eot|otf|ttf|woff|woff2)$ {
            add_header Access-Control-Allow-Origin *;
    }

    location / {
            proxy_read_timeout    90;
            proxy_connect_timeout 90;
            proxy_redirect        off;

            proxy_set_header      X-Real-IP $remote_addr;
            proxy_set_header      X-Scheme $scheme;
            proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header      X-Forwarded-Proto $scheme;
            proxy_set_header      X-Forwarded-Host $server_name;
            proxy_set_header      Host $host;
            proxy_set_header      X-Forwarded-Port 443;
            proxy_set_header      Authorization $http_authorization;
            proxy_pass_header     Authorization;

            try_files $uri $uri/ /index.php?$args;
    }

    # Pass all .php files onto a php-fpm/php-fcgi server.
    location ~ [^/]\.php(/|$) {
            add_header X-debug-message "A php file was used" always;
            # regex to split $uri to $fastcgi_script_name and $fastcgi_path
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            # This is a robust solution for path info security issue and 
            # works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)
            # if (!-f $document_root$fastcgi_script_name) {
            #         return 404;
            # }
            # Check that the PHP script exists before passing it
            # try_files $fastcgi_script_name =404;
            # Bypass the fact that try_files resets $fastcgi_path_info
            # see: http://trac.nginx.org/nginx/ticket/321
            set $path_info $fastcgi_path_info;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_intercept_errors on;
            fastcgi_pass php-app-one;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
            add_header X-debug-message "A static file was served" always;
            expires max;
            # log_not_found off;
    }

    location ~ /\. {
            deny all;
    }
}

Приложение 2:

upstream php-app-two {
    server php-app-two:9000;
}

server {
    listen 80;
    listen [::]:80;
    server_name app-two.local;
    return 301 https://$server_name$request_uri;
}

server {
    # SSL configuration
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_certificate /etc/nginx/certs/app-two.crt;
    ssl_certificate_key /etc/nginx/certs/app-two.key;
    ssl_dhparam /etc/nginx/certs/dhparam.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    server_name app-two.local;

    root /var/www/app-two;
    index index.php index.html;

    gzip_types text/plain text/css application/json application/x-javascript
               text/xml application/xml application/xml+rss text/javascript;


    # Add headers to serve security related headers
    #
    # Disable preloading HSTS for now.  You can use the commented out header line that includes
    # the "preload" directive if you understand the implications.
    # add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header Pragma "no-cache";
    add_header Cache-Control "no-cache";
    add_header X-uri "$uri";

    location ~* \.(eot|otf|ttf|woff|woff2)$ {
            add_header Access-Control-Allow-Origin *;
    }

    location / {
            proxy_read_timeout    90;
            proxy_connect_timeout 90;
            proxy_redirect        off;

            proxy_set_header      X-Real-IP $remote_addr;
            proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header      X-Forwarded-Host $server_name;
            proxy_set_header      Host $host;
            proxy_set_header      X-Forwarded-Proto $scheme;
            proxy_set_header      X-Forwarded-Port 443;
            proxy_set_header      Authorization $http_authorization;
            proxy_pass_header     Authorization;

            try_files $uri $uri/ /index.php;
    }

    # Pass all .php files onto a php-fpm/php-fcgi server.
    location ~ [^/]\.php(/|$) {
            add_header X-debug-message "A php file was used" always;
            # add_header Location "$uri" always;
            # regex to split $uri to $fastcgi_script_name and $fastcgi_path
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            # This is a robust solution for path info security issue and 
            # works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)
            if (!-f $document_root$fastcgi_script_name) {
                    return 404;
            }
            # Check that the PHP script exists before passing it
            try_files $fastcgi_script_name =404;
            # Bypass the fact that try_files resets $fastcgi_path_info
            # see: http://trac.nginx.org/nginx/ticket/321
            set $path_info $fastcgi_path_info;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_intercept_errors on;
            fastcgi_pass php-app-two;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
            expires max;
            log_not_found off;
    }
}

Обратный прокси Nginx:

worker_processes 1;

daemon off;

events {
    worker_connections 1024;
}

error_log   /var/log/nginx/error.log warn;
pid         /var/run/nginx.pid;

http {
    default_type application/octet-stream;
    include /etc/nginx/conf/mime.types;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;

    keepalive_timeout 65;

    gzip on;
    gzip_disable "msie6";

    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    application/x-font-ttf ttc ttf;
    application/x-font-otf otf;
    application/font-woff woff;
    application/font-woff2 woff2;
    application/vnd.ms-fontobject eot;

    include /etc/nginx/conf.d/*.conf;
}

докер-compose.yml:

version: '3.3'
services:
  # configured to act as a proxy for wp and member portal
  nginx:
    image: evild/alpine-nginx:1.9.15-openssl
    container_name: nginx
    # volumes offer persistent storage
    volumes:
      - ./app_one:/var/www/app_one/:ro
      - ./app_two:/var/www/app_two/:ro
      - ./nginx/conf/nginx.conf:/etc/nginx/conf/default.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certs:/etc/nginx/certs
    # ports to bind to
    ports:
      - 80:80
      - 443:443
    # allows service to be accessible by other docker containers
    expose:
      - "80"
      - "443"
    depends_on:
      - php-app_one
      - php-app_two
    environment: 
      TZ: "America/Los_Angeles"


  # app-two php container
  php-app_two:
    environment: 
      TZ: "America/Los_Angeles"
    image: joebubna/php
    container_name: app_two_php
    restart: always
    volumes:
      - ./app_two:/var/www/app_two
    ports:
      - 9000:9000

   php-app_one:
     environment: 
       TZ: "America/Los_Angeles"
     image: joebubna/php
     container_name: app_one_php
     restart: always
     volumes:
       - ./app-one:/var/www/app-one
     ports:
       - 9001:9000

  db:
    image: mysql:5.6
    container_name: app_two_mysql
    volumes:
      - db-data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/ZZ-app-one.cnf:ro
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: cora
      TZ: "America/Los_Angeles"
    ports:
      - 3306:3306
    expose:
      - "3306"

    volumes:
      db-data:

В приложении 1 и приложении 2 включен SSL с самозаверяющими сертификатами, которые импортируются при создании с помощью docker-compose.

Приложение 1 имеет несколько конечных точек API, к которым должно получить доступ приложение 2. Когда я пытаюсь получить доступ через Guzzle, я получаю:

Fatal error: Uncaught GuzzleHttp\Exception\ConnectException: cURL error 7: Failed to connect to app-one.local port 443: Connection refused (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) in /var/www/app/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php on line 185

GuzzleHttp\Exception\ConnectException: cURL error 7: Failed to connect to app-one.local port 443: Connection refused (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) in /var/www/app/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php on line 185

Call Stack:
0.0026     366656   1. {main}() /var/www/app/index.php:0
0.2229    3355944   2. Cora\Route->routeProcess() /var/www/app/index.php:45
0.2230    3357208   3. Cora\Route->routeFind() /var/www/app/vendor/cora/cora-framework/system/classes/Route.php:89
0.2240    3357912   4. Cora\Route->routeFind() /var/www/app/vendor/cora/cora-framework/system/classes/Route.php:474
0.2245    3358576   5. Cora\Route->getController() /var/www/app/vendor/cora/cora-framework/system/classes/Route.php:441
0.2364    3477872   6. Controllers\Api\Dashboard->__construct() /var/www/app/vendor/cora/cora-framework/system/classes/Route.php:501
0.2984    4086336   7. GuzzleHttp\Client->get() /var/www/app/controllers/api/controller.Dashboard.php:36
0.2984    4086712   8. GuzzleHttp\Client->__call() /var/www/app/controllers/api/controller.Dashboard.php:36
0.2984    4086712   9. GuzzleHttp\Client->request() /var/www/app/vendor/guzzlehttp/guzzle/src/Client.php:89
0.3521    4321000  10. GuzzleHttp\Promise\RejectedPromise->wait() /var/www/app/vendor/guzzlehttp/guzzle/src/Client.php:131

Вот как я сейчас реализую клиент (включая часть кода, который я добавил в своих попытках исправить это):

<?php
namespace Controllers\Api;

use \GuzzleHttp\Client;
// use \GuzzleHttp\Psr7\Uri;

define('URL', 'https://app-one.local/api/');

class Dashboard extends ApiController 
{
    private $http;

    public function __construct($container)
    {
        // We're using guzzle for our requests to help keep opportunity
        // for cURL errors to a minimum
        $this->http = new Client([
            'base_uri'          => URL,
            'timeout'           => 30.0,
            'allow_redirects'   => true,
            'verify'            => false,
            'curl'              => [
                 CURLOPT_VERIFYPEER => false
            ],
            'headers'           => [
                'User-Agent'        => 'curl/7.38.0',
            ],
        ]);

        $response = $this->http->get('member/sales/hasalestest');
        var_dump($response);
        exit;
    }
}

Как я уже упоминал, я могу получить доступ к этой конечной точке через браузер и получить к ней прямой доступ с помощью cURL в терминале, пока я использую флаг -k для «небезопасного». Я не уверен, что еще я могу сделать, поскольку документация Guzzle не очень ясна в отношении различий в синтаксисе между 5 и 6. Тогда у толпы Drupal и Laravel, как правило, есть несвязанные проблемы.

Этот пост SO выглядел похожим (за исключением жестко закодированного номера порта и Guzzle v.5), но не упоминает ничего, что я не пробовал: PHP Guzzle 5: Не удается обработать URL-адрес с номером PORT в нем .

Этот вопрос также представляет интерес, но на основе других приложений, которые взаимодействуют с приложением 1, он позволяет другим приложениям использовать определенные конечные точки API: ошибка cURL 7: не удалось подключиться к порту 443 maps.googleapis.com

Все, о чем я могу думать на данный момент, может быть, это проблема с конфигурацией nginx? Толчок в правильном направлении — это все, что мне нужно, чтобы двигаться вперед и получить остальные конечные точки, которые мне нужно потреблять, потребляя.

Спасибо за любое руководство!


person TomJ    schedule 18.04.2018    source источник
comment
Могут ли два контейнера приложений взаимодействовать друг с другом?   -  person csb    schedule 18.04.2018
comment
@csb Думаю, да. Я обновлю вопрос с конфигурациями nginx.   -  person TomJ    schedule 18.04.2018


Ответы (3)


Проблема в том, что ваш файл hosts на вашем локальном компьютере не повлияет на то, как экземпляры докера сопоставляют IP-адрес с хостом.

Попробуйте получить доступ к конечным точкам через имя контейнера...

person cherrysoft    schedule 18.04.2018
comment
Я не уверен, что понимаю, что вы здесь говорите. Есть ли способ доступа к конечным точкам по имени контейнера через php? Я думал, что обратное проксирование nginx позаботится о сопоставлении URI с соответствующим контейнером. - person TomJ; 18.04.2018
comment
Привет. Да, при условии, что контейнеры подключены к мосту по умолчанию, каким они должны быть. Возможно, начните с проверки этого, запустив «docker network inspect bridge» — вы должны увидеть свои контейнеры в этом мосту. Вы увидите их IP-адреса. Попробуйте также с IP (для устранения неполадок). - person cherrysoft; 18.04.2018
comment
Итак, после долгих экспериментов и изучения документации, а также привлечения специалиста по devops из моей компании, мы обошли эту ошибку. Теперь у меня есть еще одна ошибка, связанная с кодовой базой. В итоге нужно было создать оверлейную сеть и создать псевдонимы для каждого приложения. Это позволило им начать общаться друг с другом. Спасибо за вашу помощь @cherrysoft! - person TomJ; 19.04.2018
comment
Рад, что вы разобрались и +1 за добавление решения. - person cherrysoft; 22.04.2018

Это оказалось относительно простым решением. Проблема заключалась в том, что два контейнера fpm не знали друг о друге, и, ссылаясь на app-one.local в запросе app-two, app-two фактически отправлял запрос в пустоту. Исправление для этого было следующим:

version: '3.3'
services:
  nginx:
    image: evild/alpine-nginx:1.9.15-openssl
    container_name: nginx
    volumes:
      - ./app-one:/var/www/app-one/:ro
      - ./app-two:/var/www/app-two/:ro
      - ./nginx/conf/nginx.conf:/etc/nginx/conf/default.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certs:/etc/nginx/certs
    ports:
      - 80:80
      - 443:443
    expose:
      - "80"
      - "443"
    depends_on:
      - app-one
      - app-two
    environment: 
      TZ: "America/Los_Angeles"
    # This is the fix
    networks:
      default:
        aliases:
          - app-one.local
          - app-two.local

  app-one:
    environment: 
      TZ: "America/Los_Angeles"
    image: joebubna/php
    container_name: app-one
    restart: always
    volumes:
      - ./app-one:/var/www/app-one
    ports:
      - 9000:9000
    # This is the fix
    networks:
      - default

  app-two:
    environment: 
      TZ: "America/Los_Angeles"
    image: joebubna/php
    container_name: app-two
    restart: always
    volumes:
      - ./app-two:/var/www/app-two
    ports:
      - 9001:9000
    # This is the fix
    networks:
      - default

  db:
    image: mysql:5.6
    container_name: mysql
    volumes:
      - db-data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/ZZ-mysql.cnf:ro
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: cora
      TZ: "America/Los_Angeles"
    ports:
      - 3306:3306
    expose:
      - "3306"
    # This is the fix
    networks:
      - default

volumes:
  db-data:
# This is the fix
networks:
  default:
    driver: bridge

В итоге я создал оверлейную сеть и сообщил контейнеру nginx о каждом доменном имени fpm. Это позволяет двум контейнерам теперь отправлять запросы друг другу через полное доменное имя, а не через IP или идентификатор/имя контейнера. Простая вещь, которую нельзя упускать из виду задним числом.

person TomJ    schedule 19.04.2018

В моем случае URL-адрес недействителен, мне не хватало «https://» в начале URL-адреса. при добавлении было нормально

person Ahmed Awan    schedule 15.10.2018