pysmbの使い方（接続・切断）

pysmbについて[以前書いた記事](https://qiita.com/t2kojima/items/250d68c56a8c9fe95f52)がだいぶざっくり目になっていたことと、いくつか不要な設定が混じっていたりしたので、改めて初心者向けに網羅的な内容で書き直しました。

# pysmbとは

Pythonでファイル共有を利用できるモジュールで、SMB/CIFSクライアントとして動作します。

公式サイト [https://miketeo.net/wp/index.php/projects/pysmb](https://miketeo.net/wp/index.php/projects/pysmb)
GitHubリポジトリ [https://github.com/miketeo/pysmb](https://github.com/miketeo/pysmb)
公式ドキュメント [http://pysmb.readthedocs.io/en/latest/index.html](http://pysmb.readthedocs.io/en/latest/index.html)

# インデックス
pysmbの使い方（接続・切断）
[pysmbの使い方（ファイル受信）](https://qiita.com/t2kojima/items/6c20a7801467221470ac)
[pysmbの使い方（ファイル送信）](https://qiita.com/t2kojima/items/a6b81de771ac45c9948b)
[pysmbの使い方（ロギング）](https://qiita.com/t2kojima/items/4fc5e2e2c041b2c16824)
[pysmbの使い方（匿名接続）](https://qiita.com/t2kojima/items/c84a803c73daf043f053)

# 実行環境

#### クライアント
- Windows 10
- Python 3.6.3
- pysmb 1.1.22

#### サーバー
- Windows 7
    - User : IEUser
    - Pass : Passw0rd!
    - HostName : IEWIN7
    - Domain : WORKGROUP
    - IPAddress : 172.28.0.198

ちなみにサーバーは[Microsoftから無償提供されている仮想マシン](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/)を使いました。

# インストール

安定バージョンは`pip`からインストールできます。

```
pip install pysmb
```

## GitHubからインストール

GitHubからインストールすることもできます。

```
pip install git+https://github.com/miketeo/pysmb
```

pysmbバージョン1.1.22の場合、listPath()メソッドを呼んだ時`UnicodeDecodeError`になる不具合[(#104 Decode of short_name in decodeQueryStruct sometimes fails)](https://github.com/miketeo/pysmb/issues/104)がありますが、`master`リポジトリには既に修正が反映されていますので、GitHubの`master`をインストールするのがおススメです。[^1]

# 最小接続サンプル

サーバーにSMBで接続してEchoするだけのサンプルです。

```python
# main.py

import platform
from smb.SMBConnection import SMBConnection

conn = SMBConnection(
    'IEUser',
    'Passw0rd!',
    platform.uname().node,
    'IEWIN7')
conn.connect('172.28.0.198', 139)

print(conn.echo('echo success'))

conn.close()
```

実行すると以下のようになります。

```
> python main.py
echo success
```

内容を一つずつ見ていきましょう。

## SMBConnectionkクラスのインスタンスを生成

```python
conn = SMBConnection(
    'IEUser',
    'Passw0rd!',
    platform.node(),
    'IEWIN7')
```

`SMBConnection`インスタンスを生成します。`__init__()`の引数は以下の通りです。

```python
def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False):
```

必須の引数

- username  
サーバーのユーザー名、今回は`IEUser`を指定します。
- password  
サーバーのパスワード、今回は`Passw0rd!`を指定します。
- my_name  
クライアントのNetBIOS名です。`platform.node()`から取得すると楽です。
- remote_name  
サーバーのNetBIOS名、今回は`IEWIN7`を指定します。  

任意の引数（基本全て未設定でOK）

- domain  
ネットワークドメインですが、通常空文字で問題ありませんし、デフォルトも空文字です。
- use_ntlm_v2  
`NTLMv2`を利用するかどうか。通常`True`で問題ないと思いますが、古い環境で`NTLMv1`しか利用できない場合は`False`とします。
- sign_options  
SMBメッセージの署名設定です。
    - `SIGN_WHEN_REQUIRED` : サーバーが必要とする場合のみ署名する。（デフォルト）
    - `SIGN_WHEN_SUPPORTED` : サーバーが署名を必要としていなくても、署名をサポートしていれば署名する。
    - `SIGN_NEVER` : 署名しない。サーバーで署名が必要な場合はアクセスエラーになる。
- is_direct_tcp  
ダイレクトホスティングSMBを使用するかどうか。`True`の場合接続ポートが`445`になる。

## 接続

```python
conn.connect('172.28.0.198', 139)
```

サーバーへ接続します。

- ip  
サーバーのIPです。今回は`172.28.0.198`を指定。
- port  
接続ポートです。通常は`139`、`is_direct_tcp=True`の場合は`445`
- sock_family  
ソケットファミリです。デフォルトは`AF_INET`なので未指定でいいでしょう。  
`AF_INET6`とか指定すればIPv6が使えるかもしれません。試していません。
- timeout  
タイムアウト値です。デフォルト60秒

接続に失敗した場合は以下のような例外を投げます。

- `username`や`password`が間違っている場合は`NotReadyError`  
- `remote_name`が間違っている場合は`NotConnectedError`  
- `ip`や`port`が間違っている場合は`TimeoutError`

## エコー

```python
print(conn.echo('echo success'))
```

[SMB_COM_ECHO](https://msdn.microsoft.com/en-us/library/ee441878.aspx)コマンドを実行するメソッドです。サーバーとの疎通確認に使用します。

- data  
エコーする文字列を指定します。
- timeout  
タイムアウト値です。デフォルト10秒

サッと見た感じ、失敗した場合は`NotConnectedError`か`SMBTimeout`を投げます。

## 切断

```python
conn.close()
```

接続を切断する。使い終わったらちゃんと切りましょう。

ちなみにこのメソッドは内部でソケットがあるかチェックしてくれるので、問答無用で呼んでOK。



# コンテキストマネージャを適用

`close()`っていうメソッドを見るとどうしても`with`を使いたくなります。

なのでコンテキストマネージャを適用したクラスでラップしてみます。

```python
import platform
from smb.SMBConnection import SMBConnection


class Smb():

    def __init__(self, username, password, remote_name, ip):
        self.conn = SMBConnection(
            username, password, platform.node(), remote_name)
        self.ip = ip

    def __enter__(self):
        self.conn.connect(self.ip, 139)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.conn.close()

    def echo(self, data):
        return self.conn.echo(data)

```

こんな感じで使う

```python
from samba import Smb

params = {
    'username': 'IEUser',
    'password': 'Passw0rd!',
    'remote_name': 'IEWIN7',
    'ip': '172.28.0.198'
}
with Smb(**params) as smb:
    print(smb.echo("echo !!"))
```

まあ、`close()`忘れを気にしなくていいくらいで

# SMBバージョン

サーバーが`SMB2`をサポートする場合は`SMB2`を使用し、サポートしない場合は自動的に`SMB1`を使って通信します。

もし明示的に`SMB1`を使用したい場合は、SMBConnectionのインスタンスを作成する前に以下のように`SUPPORT_SMB2`フラグを**False**に設定します。

```python
from smb import smb_structs
smb_structs.SUPPORT_SMB2 = False
```

インスタンスを作成した後、どっちで通信しているかは`isUsingSMB2`プロパティで確認できます。

```python
print("used SMB2 Protcol" if conn.isUsingSMB2 else "used SMB1 Protcol")
```

# 読まなくてもいい話

## my_name

`SMBConnection#__init__`の`my_name`引数ですが、実はクライアントのNetBIOS名じゃなくても接続できます。空文字や適当な文字列でもOKです。
というのも適当な文字列（仮に`HOGE`とすると）で接続したときにパケットを盗み見たら、NetBIOS名`HOGE`としてNetBIOS Sessionが正常に完了していました。
[RFC1001](https://tools.ietf.org/html/rfc1001)を確認しましたが、リモートのNetBIOS名（`Called NetBIOS name`）は存在しなければならないと書いてありますが、クライアントのNetBIOS名（`Calling NetBIOS name`）については記述がありません。
そもそも本来NetBIOS名が違ったら通信できないハズですけど、`pysmb`ではIPで接続してるから大丈夫…なのかな？

でも引数としては必須なのでなんか設定しないとダメです。

## is_direct_tcp

`SMBConnection#connect`の`is_direct_tcp`引数ですが、`True`にするとダイレクトホスティングSMBを使用するので、NetBIOSを使用しなくなります。
つまり`my_name`と`remote_name`を使用しなくなるということで、これらの引数に何を渡しても関係なくなります。（`my_name`は元々関係ないですけど…）
その為両方`'a'`とかにしても接続できますが、やはり引数として必須なのでなんか設定しないとダメです。
ただし両方空文字は*NG*です。何かしらの文字列必要です。


[^1]: 2018/04/20現在、修正は次のバージョンに含まれる予定のようです。